DynamoDB时间轴并发插入场景事务数据完整性方案咨询
可以,但必须配合条件写入使用,仅靠原生事务的原子性无法自动解决并发插入的覆盖问题。
DynamoDB事务提供串行化隔离级别,事务内所有读写操作要么全部提交成功,要么全部回滚,不会出现半成功的中间状态。但如果不在写操作中增加条件校验,两个并发插入同一间隙的请求依然会出现数据覆盖:
比如两个请求同时要在startTime=4和startTime=8的两个条目之间插入新节点,两个事务同时读到前序节点的nextStartTime=8,随后都去更新前序节点的nextStartTime为自己的新节点时间,最后提交的事务会覆盖前一个的修改,直接导致时间轴链表断裂。
只要在更新前序节点时增加条件判断,要求更新时节点的nextStartTime必须等于事务一开始读到的原值,就可以避免这个问题:一旦有其他事务先修改了前序节点的nextStartTime,当前事务会直接触发条件校验失败、整体回滚,不会产生脏数据。
首选方案:事务+条件写入(强一致,无异常窗口)
这是最适配你当前业务场景的方案,一致性保证和SQL数据库的行锁效果等价,操作步骤如下:
- 定位插入间隙的前序节点o1、后序节点o2,通过事务内的强一致性读拿到o1的
nextStartTime,确认该值等于o2的startTime,同时确认两个节点都存在,避免插入到错误位置。 - 构造事务提交的三类操作:
- 更新o1节点:将
nextStartTime修改为新节点的startTime,增加条件表达式:attribute_exists(o1) AND nextStartTime = :originNextStart,其中:originNextStart是第一步读到的o1原nextStartTime值 - 写入新节点:字段包括
objectId、startTime(新节点时间)、endTime、nextStartTime = :originNextStart,增加条件表达式:attribute_not_exists(主键),避免同主键节点重复写入 - 增加对o2节点的存在性校验:条件为
attribute_exists(o2),避免nextStartTime指向不存在的节点
- 更新o1节点:将
- 提交事务,如果捕获到事务冲突异常、条件校验失败异常,采用指数退避策略重试整个流程:重新读取最新的相邻节点、确认插入位置、再次构造事务提交。
这个方案的一致性是强保障的,只要事务提交成功,时间轴的指针一定是连续正确的,不会出现并发插入错乱的问题。注意DynamoDB单事务最多支持100个项操作、总大小不超过4MB,你的场景每次插入仅涉及2-3个项操作,完全在限制范围内。
备选方案:版本号乐观锁(高性能,最终一致)
如果你的时间轴写入并发非常高,事务重试带来的开销无法接受,可以选择轻量的乐观锁方案:
- 给每个时间轴节点增加
version字段,每次更新节点时将version自增1 - 更新o1节点时,增加条件
version = :expectedVersion(:expectedVersion是读取到的o1版本号),校验失败则重试 - 写入新节点时同样校验主键不存在
这个方案的性能比事务高30%左右,但存在短时间的不一致窗口:如果更新o1成功后、写入新节点失败,会出现o1的nextStartTime指向不存在节点的问题,需要额外跑定时巡检任务,扫描所有nextStartTime无对应节点的异常记录做补偿修复,适合对一致性要求不是绝对严格的场景。
注:你给出的示例数据中第三条记录
(1,8,10,9)的nextStartTime=9无对应节点,属于典型的不一致脏数据,使用上述两种方案时的条件校验都可以提前拦截这类问题。
内容的提问来源于stack exchange,提问作者Siddharth

