三阶段提交在 2PC 上加了一轮 PRECOMMIT,目标是消除阻塞。它真的更好吗?这篇把两个协议写成状态模型,枚举协调者的每个崩溃位置和每种网络分区,数出两者各自的阻塞和不一致,再算出多出来的消息和延迟。
2PC:12 条消息(含确认),7 次强制落盘,从协调者开始到最后一个参与者提交 15 ms = 3 个单程延迟
10.0 ms 协调者收齐 3 票 YES,强制写 COMMIT 决定,发 COMMIT
3PC:18 条消息(含确认),10 次强制落盘,从协调者开始到最后一个参与者提交 25 ms = 5 个单程延迟
10.0 ms 协调者收齐 3 票 YES,发 PRECOMMIT
20.0 ms 协调者收齐 3 个 PRECOMMIT 的确认,强制写 COMMIT 决定,发 DOCOMMIT
3PC 比 2PC 多 6 条消息、多 3 次落盘、多 10 ms(多一个往返)3PC 在投票和提交之间加了 PRECOMMIT 一轮:多 6 条消息、多 3 次强制落盘、多一个往返。
--- 2PC(协调者共发 6 条消息,崩溃点 0 到 6)
崩溃点 0(协调者什么都没发):状态 INIT / INIT / INIT -> ABORT 组里有人还没投过票(INIT),它可以单方面 ABORT
崩溃点 1(已发 PREPARE 给 P1):状态 READY / INIT / INIT -> ABORT 组里有人还没投过票(INIT),它可以单方面 ABORT
崩溃点 2(已发 PREPARE 给 P2):状态 READY / READY / INIT -> ABORT 组里有人还没投过票(INIT),它可以单方面 ABORT
崩溃点 3(已发 PREPARE 给 P3):状态 READY / READY / READY -> BLOCKED 全部是 READY:已经投了 YES,不知道协调者的决定,既不能提交也不能回滚
崩溃点 4(已发 COMMIT 给 P1):状态 COMMIT / READY / READY -> COMMIT 组里有人已经 COMMIT
崩溃点 5(已发 COMMIT 给 P2):状态 COMMIT / COMMIT / READY -> COMMIT 组里有人已经 COMMIT
崩溃点 6(协调者已发完全部消息):状态 COMMIT / COMMIT / COMMIT -> COMMIT 组里有人已经 COMMIT
--- 3PC(协调者共发 9 条消息,崩溃点 0 到 9)
崩溃点 0(协调者什么都没发):状态 INIT / INIT / INIT -> ABORT 没有人收到 PRECOMMIT(全是 READY / INIT):不可能有人已经 COMMIT,可以 ABORT
崩溃点 1(已发 CANCOMMIT 给 P1):状态 READY / INIT / INIT -> ABORT 没有人收到 PRECOMMIT(全是 READY / INIT):不可能有人已经 COMMIT,可以 ABORT
崩溃点 2(已发 CANCOMMIT 给 P2):状态 READY / READY / INIT -> ABORT 没有人收到 PRECOMMIT(全是 READY / INIT):不可能有人已经 COMMIT,可以 ABORT
崩溃点 3(已发 CANCOMMIT 给 P3):状态 READY / READY / READY -> ABORT 没有人收到 PRECOMMIT(全是 READY / INIT):不可能有人已经 COMMIT,可以 ABORT
崩溃点 4(已发 PRECOMMIT 给 P1):状态 PRECOMMIT / READY / READY -> COMMIT 组里有人收到了 PRECOMMIT:说明所有人都投过 YES,可以 COMMIT
崩溃点 5(已发 PRECOMMIT 给 P2):状态 PRECOMMIT / PRECOMMIT / READY -> COMMIT 组里有人收到了 PRECOMMIT:说明所有人都投过 YES,可以 COMMIT
崩溃点 6(已发 PRECOMMIT 给 P3):状态 PRECOMMIT / PRECOMMIT / PRECOMMIT -> COMMIT 组里有人收到了 PRECOMMIT:说明所有人都投过 YES,可以 COMMIT
崩溃点 7(已发 DOCOMMIT 给 P1):状态 COMMIT / PRECOMMIT / PRECOMMIT -> COMMIT 组里有人已经 COMMIT
崩溃点 8(已发 DOCOMMIT 给 P2):状态 COMMIT / COMMIT / PRECOMMIT -> COMMIT 组里有人已经 COMMIT
崩溃点 9(协调者已发完全部消息):状态 COMMIT / COMMIT / COMMIT -> COMMIT 组里有人已经 COMMIT
没有分区时:2PC 有 1 / 7 个崩溃点会让参与者阻塞;3PC 有 0 / 10 个2PC 在“全部 READY、没人知道决定”的窗口里阻塞;3PC 因为 READY 状态下一定没人提交,可以安全 ABORT,枚举下来没有一个阻塞的崩溃点。
2PC:35 个场景(7 个崩溃点 × 5 种分区 ['P1|P2|P3', 'P1P2|P3', 'P1P3|P2', 'P1|P2P3', 'P1P2P3']): 正常收尾 18 阻塞 17 结果不一致 0
3PC:50 个场景(10 个崩溃点 × 5 种分区 ['P1|P2|P3', 'P1P2|P3', 'P1P3|P2', 'P1|P2P3', 'P1P2P3']): 正常收尾 44 阻塞 0 结果不一致 6
3PC 里结果不一致的场景(崩溃点 / 分区 / 各小组的决定):
崩溃点 4(已发 PRECOMMIT 给 P1) 分区 P1|P2|P3 P1 → COMMIT; P2 → ABORT; P3 → ABORT
崩溃点 4(已发 PRECOMMIT 给 P1) 分区 P1P2|P3 P1P2 → COMMIT; P3 → ABORT
崩溃点 4(已发 PRECOMMIT 给 P1) 分区 P1P3|P2 P1P3 → COMMIT; P2 → ABORT
崩溃点 4(已发 PRECOMMIT 给 P1) 分区 P1|P2P3 P1 → COMMIT; P2P3 → ABORT
崩溃点 5(已发 PRECOMMIT 给 P2) 分区 P1|P2|P3 P1 → COMMIT; P2 → COMMIT; P3 → ABORT
崩溃点 5(已发 PRECOMMIT 给 P2) 分区 P1P2|P3 P1P2 → COMMIT; P3 → ABORT
2PC 在分区下从不出现不一致:它宁可阻塞(READY 的小组什么也不决定)3PC 的“超时就决定”假设了超时一定是崩溃而不是分区。分区时两个小组各按规则收尾,会做出相反的决定;2PC 在同样的场景里只是阻塞。
协调者发出的消息:CANCOMMIT→P1, CANCOMMIT→P2, CANCOMMIT→P3, PRECOMMIT→P1;之后挂掉 此刻状态:P1=PRECOMMIT, P2=READY, P3=READY 小组 P1:超时 → COMMIT(组里有人收到了 PRECOMMIT:说明所有人都投过 YES,可以 COMMIT) 小组 P2P3:超时 → ABORT(没有人收到 PRECOMMIT(全是 READY / INIT):不可能有人已经 COMMIT,可以 ABORT) P1 提交了,P2 和 P3 回滚了:分区恢复之后这笔事务在 P1 和 P2/P3 上的结局永远对不上
PRECOMMIT 只发出去一个就挂,有 PRECOMMIT 的一方提交,其余回滚。两边各自的推理都符合协议。
协调者发出的消息:PREPARE→P1, PREPARE→P2, PREPARE→P3, COMMIT→P1;之后挂掉 此刻状态:P1=COMMIT, P2=READY, P3=READY 小组 P1:COMMIT(组里有人已经 COMMIT) 小组 P2P3:BLOCKED(全部是 READY:已经投了 YES,不知道协调者的决定,既不能提交也不能回滚) P1 已经提交,P2 和 P3 不敢动,等分区恢复后向 P1 打听到 COMMIT 再提交:代价是这段时间它们的锁一直握着,但结局是对的
2PC 在不确定时等,3PC 在不确定时猜。真实系统不靠 3PC 解决 2PC 的阻塞,而是让协调者本身不再单点(比如把协调者的状态放进共识组),这一点本篇没有验证。