bigywater.tech
分布式事务 · 4

三阶段提交为什么很少用

三阶段提交在 2PC 上加了一轮 PRECOMMIT,目标是消除阻塞。它真的更好吗?这篇把两个协议写成状态模型,枚举协调者的每个崩溃位置和每种网络分区,数出两者各自的阻塞和不一致,再算出多出来的消息和延迟。

6 / 50
3PC 在崩溃点 × 分区的 50 个场景里,各小组做出相反决定的场景数(2PC 是 0 / 35)
1 / 7 → 0 / 10
没有分区时,2PC 与 3PC 会让参与者阻塞的协调者崩溃点
+10 ms
无故障时 3PC 比 2PC 多的延迟(模型里单程 5 ms,多一个往返)

代价:多一轮

实测 · 本机执行输出
  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 次强制落盘、多一个往返。

没有分区:3PC 确实不阻塞

实测 · 本机执行输出
  --- 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 的一方提交,其余回滚。两边各自的推理都符合协议。

2PC 的取舍

实测 · 本机执行输出
  协调者发出的消息: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 的阻塞,而是让协调者本身不再单点(比如把协调者的状态放进共识组),这一点本篇没有验证。

分步演示

2PC 与 3PC
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MySQL 26.7.0 与 PostgreSQL 16.15(Homebrew,每个实验自己 initdb、自己起实例,端口 9150 以上,只监听 127.0.0.1)/ Python 3.14.8 + PyMySQL 1.2.3 + psycopg 3.3.6 / Redis 8.10.2 / Kafka 4.3.1 / RabbitMQ 4.3.6 / etcd 3.7.2(按各篇注明用到哪些);不连任何外部服务,不碰本机已有的数据库实例;故障用 kill -9 进程、让连接断开或在代码里的指定位置抛异常来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇没有用任何数据库:2PC 和 3PC 是我按教科书规则写的状态模型(tools/articles/exp/t4.py),消息、延迟和强制落盘数来自离散事件模拟(单程延迟取 5 ms),不一致 / 阻塞来自对协调者崩溃点 × 网络分区方式的穷举;3 个参与者、全部投 YES 的情形
    • 没有验证:任何真实的 3PC 实现(没有主流数据库实现它);参与者也崩溃、投 NO、多次崩溃的组合;带时钟同步假设的 3PC 变体;把协调者放进 Paxos / Raft 组之后的 2PC(业界常见做法,本篇没有实验);超时参数和真实网络的行为
    京ICP备2026067502号-1京公网安备11010502063487号