undo-do-redo 重放
祖先检查保证了单次应用不成环,但分布式下操作会乱序到达。每个副本给操作打一个全序时间戳,采用 Lamport 时钟的「计数器加副本 id」形式,计数器相同再比副本 id 打破平局,并维护一份按时间戳升序的 op log。关键技巧是:当一个时间戳较小的 op 迟到,不能直接追加,而要先把 log 末尾所有时间戳更大的 entry 依次 undo 回去、do 这条迟到的 op、再按时间戳升序 redo 回来。
1 · 三种到达顺序下的重放
这样无论 op 以什么顺序到达,每个副本的 log 始终维持同一个时间戳升序序列,等价于一次性按时间戳顺序从头重放,于是结果只由这组 op 决定而与到达顺序无关。并发地 move 同一节点时,就由时间戳最大者胜出,即 LWW。
警示 · 等价于「按时间戳顺序重放」这一点要归纳来看:每次 applyOp 结束时 log 都恰好是「已知全部 op 按时间戳升序」的序列,且树正好是把这个序列从头 doOp 一遍的结果。新 op 无论何时到达,undo-do-redo 都把它插回它该在的时间戳位置,并让它之后的 op 在新状态下重新 doOp。所以同一条 op 在不同重放里可能这次生效、那次因成环被拒,但最终序列唯一,最终树也就唯一,这正是强最终一致性(strong eventual consistency)。
把祖先检查与 undo-do-redo 装进多个副本、再配一条可乱序的网络,就是多副本模拟器。