在我开始之前快速澄清一下。我假设一个分布式的、面向服务的架构,并且我假设对这个 DynamoDB 表的所有读取和写入都只通过一个服务发生。我将使用“应用程序”来指代您正在构建的访问表的软件,并使用“客户端”来指代任何属于您的应用程序客户端的东西。
是否有任何内置选项,例如条件更新,但用于插入项目?
简短的回答是“是”,使用基于version numbers 的optimistic locking。
您需要首先将排序键更改为顺序事件编号。这将是用于对您的项目进行版本控制的属性,它使用条件更新,并且在解决您的问题的任何解决方案中产生的额外开销最少。
让我们首先查看建议架构的一些示例数据。我冒昧地添加了更多的状态类型。
invoiceId | eventNo | eventStatus | datetime
----------|---------|-------------|---------------------
111 | 0 | created | 2019-07-11T00:01:00
111 | 1 | approved | 2019-07-11T00:02:00
111 | 2 | modified | 2019-07-12T00:03:00
111 | 3 | approved | 2019-07-12T00:04:00
111 | 4 | settled | 2019-07-13T00:05:00
乐观锁定的一般思想是读取当前状态,然后通过插入带有递增 eventNo(相当于 AWS 文档中的 version)的新记录来更新状态,条件是 @ 987654336@ 尚不存在于该invoiceId 中。这样做的原因是,当您读取现有状态时,您总是知道下一个 eventNo 应该是什么(与使用时间戳作为排序键不同)。
为了更具体一点,在2019-07-13 上,当客户端发送结算发票的请求时,您的应用程序会读取最新状态,看到eventNo 为3,status 为“已批准”,所以它向 DynamoDB 提交了一个UpdateItem 请求(翻译成简单的英语)说
仅当invoiceId=111 和eventNo=4 不存在其他状态更新时,才插入带有invoiceId=111 和eventNo=4 的状态更新
如果两个客户端同时尝试更新状态,只有一个 UpdateItem 请求会成功,另一个会返回 ConditionalCheckFailedException。
好的,我该如何编写代码?
我已经十多年没有使用过 C#,所以请原谅可能存在的任何语法或格式错误。
AmazonDynamoDBClient client = new AmazonDynamoDBClient();
// These should be method parameters/args, but I'm directly assigning them to
// keep this code sample simple.
var invoiceToUpdate = 123;
var invoiceNewState = "settled";
// Here's the useful part of the sample code
// First we make a query to get the current state
var queryRequest = new QueryRequest
{
TableName = "Invoices",
KeyConditionExpression = "invoiceId = :inv",
ExpressionAttributeValues = new Dictionary<string, AttributeValue> {
{":inv", new AttributeValue {N = invoiceIdToUpdate.toString() }}
},
// This assumes we only need to check the current state and not any of the historical
// state, so we'll limit the query to return only one result.
Limit = 1,
// If we're limiting it to only one result, change the sort order to make sure we get
// the result with the largest eventNo (and therefore the most recent state).
ScanIndexForward = false,
// This is not strictly necessary for correctness because of the condition expression
// in the PutItem request, but including it will help reduce the likelihood of getting
// a ConditionalCheckFailedException later on.
ConsistentRead = true
};
var queryResponse = client.Query(queryRequest);
// Check to see if there is any previous record for this invoice
// Setup the default values if the query returned no results
int newEventNo = 0;
string invoiceCurrentState = null;
if (queryResponse.Items.Count > 0) {{
// If there is any existing record, then increment the eventNo for the new record
var latestRecord = queryResponse.QueryResult().Items[0];
newEventNo = Convert.ToInt32(latestRecord["eventNo"]) + 1;
invoiceCurrentState = latestRecord["eventStatus"];
}
var isValidChange = MyBusinessLogic.isValidChange(invoiceCurrentState, invoiceNewState);
if (isValidChange) {
var putItemRequest = new PutItemRequest
{
TableName = "Invoices",
Item = new Dictionary<string,AttributeValue>() {
{ "invoiceId", new AttributeValue {N = invoiceIdToUpdate.toString() }},
{ "eventNo", new AttributeValue {N = newEventNo.toString()}},
{ "eventStatus", new AttributeValue {S = invoiceNewState}},
{ "datetime", new AttributeValue {S = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") }}
},
// Every item must have the key attributes, so using 'attribute_not_exists'
// on a key attribute is functionally equivalent to an "item_not_exists"
// condition, causing the PUT to fail if it would overwrite anything at all.
ConditionExpression = "attribute_not_exists('invoiceId')"
};
try {
var putItemResponse = client.PutItem(putItemRequest);
} catch (ConditionalCheckFailedException ex) {
// How you handle this is up to you. I recommend choosing one of these options:
// (1) Throw an exception with a more useful message explaining that the state changed while the
// request was being processed
// (2) Automatically try again, starting with the query and including the business validations,
// and if the state change is still valid, submit a new PutItem request with the new eventNo.
}
// Return an acknowledgement to the client
} else {
throw new System.InvalidOperationException("Update is not valid for the current status of the invoice.");
}
这是我给出的代码示例的一些相关文档。
是否可以锁定整个分区以防止插入来自其他客户端的新项目?
是的,但它不是在数据库中实现的锁。锁定必须在您的应用程序中使用会产生额外开销的单独锁定库进行,因此除非您没有其他选择,否则不应使用此方法。对于无法更改表架构的任何阅读问题的人,您可以将应用程序设置为使用DynamoDB lock client 锁定分区键,读取当前状态,执行写入(如果允许),然后释放锁。