DynamoDB Lambda实现乐观锁并发写入覆盖问题排查
问题根因
你的乐观锁实现思路本身是正确的,DynamoDB的ConditionExpression会基于写入前的最新数据做原子校验,出现覆盖问题完全是代码实现的3个低级错误导致的:
- 异步读取未加await,版本号取值完全错误
你调用getItem(id)时没有加await关键字,此时item变量拿到的是异步请求的Promise对象,而非数据库返回的实际条目。你后续访问item.VersionAttribute得到的是undefined,传入条件表达式的版本号是无效值,部分版本的AWS SDK在序列化参数时会自动剔除值为undefined的属性,直接导致ConditionExpression失效,相当于没有加任何版本校验就执行了更新。 - 新配置变量引用错误
你写了item.config = newConfig(),但更新参数里的:config引用的是当前作用域未定义的config变量。如果这个config是外层作用域的共享变量,并发场景下后执行的逻辑会覆盖前面生成的配置值,直接导致写入的配置不符合预期。 - 异常捕获逻辑完全不做校验
你在catch块中无差别捕获所有异常就直接重试,既不判断是否为预期的ConditionalCheckFailedException(版本冲突错误),也不打印错误日志,参数错误、网络错误等问题会被直接吞掉,甚至重试5次失败后也没有任何报错,你根本感知不到更新逻辑的异常。
另外补充一点:DynamoDB默认的GetItem是最终一致性读,可能返回旧版本数据,虽然不会导致写条件失效,但会增加不必要的重试次数,乐观锁场景下建议读取时开启强一致性读。
修正后的代码
const id = 1; const MAX_RETRIES = 5; let retries = 0; while (retries < MAX_RETRIES) { // 加await等待读取完成,开启强一致性读拿到最新版本 const item = await getItem(id, { ConsistentRead: true }); // 正确声明新配置变量,不要挂载到item对象上 const newConfigVal = newConfig(); const currentVersion = item.VersionAttribute; const params = { TableName: process.env.tableName, Key: { Id: id }, // 版本校验条件逻辑不变 ConditionExpression: '#versionAttr = :currentVersion', UpdateExpression: 'set #config = :newConfig, #versionAttr = :nextVersion', ExpressionAttributeNames: { '#versionAttr': 'VersionAttribute', '#config': 'Config' }, ExpressionAttributeValues: { ':currentVersion': currentVersion, ':nextVersion': currentVersion + 1, ':newConfig': newConfigVal } }; try { await dynamoDb.update(params).promise(); break; } catch (err) { // 只对版本冲突错误做重试,其他错误直接抛出 if (err.code !== 'ConditionalCheckFailedException') { throw err; } retries++; // 加递增延迟避免高并发下的活锁 await new Promise(resolve => setTimeout(resolve, 50 * retries)); } } if (retries >= MAX_RETRIES) { throw new Error('更新失败,超过最大重试次数'); }
补充说明
- DynamoDB的ConditionExpression校验是写入时的原子操作,不存在并发下两个请求同时通过版本=1校验的可能,只要条件表达式正确生效,后写入的请求一定会被拦截。
- 重试时建议加递增延迟,避免高并发下多个请求同时重试再次产生冲突。
- 不要在代码里无差别吞异常,非版本冲突的错误(比如权限问题、参数错误、表不存在)重试没有任何意义,直接抛出即可。
内容的提问来源于stack exchange,提问作者tomas guzman
相关产品推荐
相关产品推荐

