把「多个参与者要么都做、要么都不做」交给 ZooKeeper 来协调:投票、决定都存在节点里,崩溃后别的进程能接着做。这篇我自己写一个最小的两阶段提交,分别验证全票通过、有人反对、参与者崩溃、协调者崩溃四种情形。
20 个事务中应用完成的:20;耗时(ms)中位 52,最小 35,最大 61 一个事务在 ZooKeeper 里的节点: ['decision', 'prepare', 'votes'] | votes: ['P1', 'P2', 'P3'] | decision = commit
ZooKeeper 在这里的作用是一个可靠、有序、可 watch 的共享记录板。它自己不是事务协调器,协议是我写的最小实现(没有处理参与者重复投票、网络分区下的 watch 丢失等)。每个事务三个节点:prepare、votes(每个参与者一个子节点)、decision。
COORD PREPARE_SENT tno
P2 PREPARE tno
P1 PREPARE tno
P3 PREPARE tno
P1 VOTE tno yes
P2 VOTE tno no
P3 VOTE tno yes
COORD VOTES tno {'P2': 'no', 'P3': 'yes'}
COORD DECISION tno abort
P1 APPLIED tno abort
P2 APPLIED tno abort
P3 APPLIED tno abort
decision = abort;应用了 abort 的参与者:['P1', 'P2', 'P3']协调者看到 no 就不再等其余的票(日志里 VOTES 只记录了 P2 和 P3 的票,P1 的票虽然也写了但协调者没来得及读)。
协调者用时 3.1 s(投票超时 3 s);decision = abort;votes 目录里有 ['P1', 'P2']
P1、P2 应用了: [('P1', 'APPLIED tdie abort'), ('P2', 'APPLIED tdie abort')]
/members 现在: ['P1', 'P2'] (协调者已等了 3 秒,P3 的会话已过期、临时节点已消失)没有票就不能提交,所以超时视为 no。超时设短了会误杀慢参与者,设长了让事务久悬(这期间参与者持有的资源锁不能释放)。
协调者被杀后 5 秒:/tx/tcoord/decision 存在 = False;已应用的参与者数 = 0;votes = {'P1': 'yes', 'P2': 'yes'}
+ 0.09s RECOVER RECOVER_VOTES {'P1': 'yes', 'P2': 'yes'}
+ 0.10s RECOVER RECOVER_DECISION commit
+ 0.10s P2 APPLIED tcoord commit
+ 0.10s P1 APPLIED tcoord commit
恢复者读出所有票为 yes → 写 commit;参与者应用数 = 2阻塞问题是 2PC 本身固有的,ZooKeeper 缓解了其中一部分:投票持久地保存在节点里,所以任何进程都能读到完整的票并替协调者做决定(如果所有票都在,就能确定结果)。两个写 decision 的人都用 create,按设计后写的会得到 NodeExists、从而决定只有一个(这个并发竞争我没有测)。但如果有参与者的票没来得及写,恢复进程只能选 abort。