DynamoDB事务中乐观锁是否冗余?事务冲突异常处理疑问
问题背景
我用APIGateway、Lambda、DynamoDB结合NodeJS和TypeScript搭建了无服务器应用,碰到多客户端同时修改资源的竞态问题。单条记录修改时,我用乐观锁(以Date.now()作为版本号)处理:先查询记录确定修改逻辑,条件检查失败就重新查询调整。
现在有个场景需要同时更新两条记录:先查询两条记录确定修改方式,再执行更新。如果期间任意一条被其他客户端修改,就得重新查询调整逻辑。我用包含两个更新操作的DynamoDB事务来解决,每个操作都加了独立的条件表达式,确保失败时全部回滚。
翻DynamoDB文档看到,当TransactWriteItems请求和其他针对同一资源的TransactWriteItems操作冲突时,会抛出TransactionCanceledException,冲突事务失败需要重试。
我的疑问:
- 事务里加乐观锁是不是完全冗余?
- 只捕获
TransactionCanceledException就够处理竞态了吗? - 或者这种冗余实现仍有价值?比如查询两条记录后、启动事务前的间隙,如果记录被其他事务修改了该怎么处理?
解决方案与分析
核心结论:事务中的乐观锁不是冗余,二者是互补关系
1. 只靠TransactionCanceledException无法覆盖所有竞态场景
TransactionCanceledException仅处理事务之间的冲突——也就是当你的TransactWriteItems和另一个TransactWriteItems操作同一资源时才会触发。但如果在你查询记录之后、启动事务之前,有非事务性的写入(比如普通的PutItem/UpdateItem)修改了记录,这时候事务不会抛出TransactionCanceledException(因为对方不是事务操作)。
这种情况下,只有在事务的每个更新操作里添加乐观锁的条件表达式(比如version = :expectedVersion),才能检测到记录已被修改,进而让整个事务失败回滚。
2. 乐观锁是处理“查询-修改-写入”间隙竞态的关键
你提到的查询后、事务前的间隙,正是乐观锁要解决的核心场景:
- 你查询到两条记录的版本号是v1和v2
- 间隙里有其他操作把其中一条改成了v3
- 启动事务时,如果每个更新操作都带了
version = :v1/version = :v2的条件,DynamoDB会直接拒绝更新,事务回滚 - 此时你可以捕获条件检查失败的异常(不是
TransactionCanceledException),然后重新查询、调整逻辑再重试
如果没加乐观锁,事务会直接覆盖掉间隙里的修改,导致数据不一致。
3. TransactionCanceledException处理事务级冲突
当两个事务同时操作同一资源时,DynamoDB会通过事务锁机制检测冲突,抛出TransactionCanceledException。这时候不管你有没有加乐观锁,冲突的事务都会失败。但加了乐观锁的话,能在事务内部更精准地控制修改的有效性,避免不必要的事务重试——比如如果事务里的某条记录在间隙已经被改了,乐观锁会直接让事务失败,不用等到事务级冲突检测。
具体实践建议
- 必须保留事务中每个更新操作的乐观锁条件表达式:覆盖非事务操作带来的间隙竞态,以及事务内部的记录级冲突。
- 同时捕获两种异常:
- 捕获
ConditionalCheckFailedException:对应单条记录在间隙被修改的场景,需要重新查询后重试。 - 捕获
TransactionCanceledException:对应事务之间的冲突,直接重试整个事务即可。
- 捕获
- 注意
Date.now()作为版本号的局限性:如果并发极高,可能出现同一毫秒内多个请求生成相同版本号,导致乐观锁失效。建议改用原子递增的数字版本号(比如用UpdateExpression里的ADD version :incr)。
内容的提问来源于stack exchange,提问作者Phil

