【发布时间】:2022-01-21 07:24:59
【问题描述】:
我遇到了一个问题,即两个并发进程在 5 毫秒内更新 DynamoDB 表,并且当我希望一个进程抛出 ConditionalCheckFailedException 异常时,它们都传递了条件表达式。文档状态:
DynamoDB 支持分布式锁所必需的机制,例如条件写入。
https://aws.amazon.com/blogs/database/building-distributed-locks-with-the-dynamodb-lock-client/
我的表架构有一个名为“Id”的 Key 属性:
AttributeDefinitions:
-
AttributeName: "Id"
AttributeType: "S"
KeySchema:
-
AttributeName: "Id"
KeyType: "HASH"
我的条件表达式是:
string conditional = "attribute_not_exists(StartedRefreshingAt)";
包含条件表达式的Lock方法:
private bool Lock(Configuration config)
{
string conditional = "attribute_not_exists(StartedRefreshingAt)";
Dictionary<string, AttributeValue> values = new Dictionary<string, AttributeValue>{
{":new_refresh", new AttributeValue(ToDDBDate(DateTime.Now))}};
try
{
_dynamoDB.UpdateItemAsync(new UpdateItemRequest
{
TableName = TABLE_NAME,
Key = new Dictionary<string, AttributeValue>{{"Id", new AttributeValue(config.Id)}},
UpdateExpression = "set StartedRefreshingAt = :new_refresh",
ConditionExpression = conditional,
ExpressionAttributeValues = values
}).Wait();
return true;
}
catch (Exception)
{
return false;
}
}
如果它返回 true,我认为表已锁定,因为 StartedRefreshingAt 属性现在存在。
在对其他属性的记录完成一些其他更新后,我再次删除了StartedRefreshingAt 属性,有效地释放了锁。这是使用lock方法的方法:
private async Task<Configuration> RefreshAsync(Configuration config)
{
// concurrent executions may enter here with the same value
// for config
if (config.AccessTokenExpired()) // Validates the age of the config.AccessToken
{
_logger.LogInformation($"Refreshing expired access token for {config.Id}");
if (Lock(config))
{
// The below code should not be executed concurrently
var authPayload = new List<KeyValuePair<string, string>>();
authPayload.Add(new KeyValuePair<string, string>("grant_type", "refresh_token"));
authPayload.Add(new KeyValuePair<string, string>("refresh_token", config.RefreshToken));
// 3rd party REST API call
// IF execution 1 completed and saved first, the below API
// call should fail with an "invlaid grant" response
// since it will be using a stale refresh token.
JObject authResponse = await GetAuthTokensAsync(authPayload);
// Bad practice, but print the auth tokens to logs here
// in case we need to recover a RefreshToken.
_logger.LogInformation($"Got auth token response for {config.Id}:" +
$" {authResponse.ToString(Formatting.None)}");
config.AccessToken = authResponse["access_token"].ToString();
config.AccessTokenCreated = DateTime.Now;
// RefreshToken is updated whenever the API call succeeds.
config.RefreshToken = authResponse["refresh_token"].ToString();
config.RefreshTokenCreated = config.AccessTokenCreated;
config.StartedRefreshingAt = null; // lock attribute
await Save(config); // saves to DynamoDB, releasing the lock.
}
else
{
_logger.LogInformation($"Someone else is refreshing {config.Id} at same time, so ignoring and returning current value");
// this will return a potentially stale config object, which client code is expected to handle
}
}
return config;
}
大多数时候,当两个并发执行运行时,此代码在两个执行中的一个上成功返回 false,平均每天大约 20 次。这似乎表明表达式正在正确评估。但大约每 2 周一次,并发执行返回 true。
编辑:在查看答案并提供更多代码上下文后,问题很可能不是 DynamoDB,执行 2 的条件写入在执行 1 释放锁定后成功。这可能是第 3 方 OAuth 服务器的一致性问题。
现在我认为问题在于执行 2 使用与执行 1 相同的刷新令牌成功完成了 OAuth API 调用。这应该是不可能的。
最终结果是我在 DynamoDB 中保存了一个 RefreshToken,它永远返回“无效授权”,这意味着它已经过时了。如果我通过日志查看日志行“Got auth token response for”何时由两个非常接近的执行写入,我可以使用首先记录的 RefreshToken 手动更新 DynamoDB 表,它会恢复。
这个场景对于竞态条件来说非常经典:
- 执行1设置StartedRefreshingAt属性并返回true,继续完成额外的工作
- 执行 2 设置 StartedRefreshingAt 属性并返回 true,继续完成额外的工作
- 执行 2 将附加工作的结果写回表中
- 执行 1 将附加工作的结果写回表中,覆盖执行 2 完成的最新工作。
有时第 3 步和第 4 步也可能反向执行,但这对我来说并不存在一致性问题,因为最近完成的工作是期望的最终结果。
【问题讨论】:
标签: c# amazon-dynamodb