目录
[为什么需要 3PC?](#为什么需要 3PC?)
[3PC与 Paxos / Raft 对比](#3PC与 Paxos / Raft 对比)
本篇文章内容的前置知识为 分布式2PC理论,如果不了解,可点击链接学习
为什么需要 3PC?
1) 2PC 的根本问题:阻塞 + 不确定
2PC 两阶段:投票(prepare)→提交(commit)
一旦协调者在关键窗口宕机或网络分区,参与者会进入不确定状态------既不能提交也不敢回滚,只能无限等待协调者的最终决定。这带来两类问题:
阻塞:协调者挂了或网络中断,参与者无法自决,业务线程/锁资源被长期占用
进退两难:参与者贸然提交会破坏原子性;贸然回滚也可能与其他节点的提交相冲突
工程副作用:长时间占锁、长事务、资源饥饿、吞吐下降,甚至雪崩。
直观例子:协调者收齐所有YES后挂了。2PC 里参与者都已准备就绪,但没有最终指令,谁也不敢动,导致系统卡死。
2) 3PC 的设计目标:在可判定超时的条件下非阻塞
3PC(Three-Phase Commit)的目标不是更强一致,而是降低阻塞:
在一个同步或部分同步网络(消息延迟有明确上限 Δ)下,引入超时与一个缓冲阶段,让参与者在协调者失联时能自行做决定,不再无限等待。
3PC的关键前提假设(很重要):
故障模型为停机型(Fail-Stop):节点故障后只会停止服务,不会发送错误消息(即不作恶)
消息延迟有上限:网络传输、节点处理的延迟不会超过 Δ,因此超时可作为协调者失联的有效判定依据
无严重网络分区:若出现分区(部分节点断连),3PC 的一致性可能被破坏(见第 5 节)
同步网络:消息从发起到接收的延迟,一定≤某个明确的固定值 Δ,且节点处理消息的速度也有上限,全程可预判。
部分同步网络:大部分时候消息延迟≤Δ,但允许偶尔超出(可恢复),核心是多数情况下能按同步网络的规则决策。
3) 3PC 的核心改动:把 2PC 的准备再切半
3PC 的核心改动是将 2PC 的 Prepare 阶段拆分为 CanCommit(预询问)和 PreCommit(预提交)两个阶段,再保留 DoCommit(正式提交)阶段,通过三阶段日志 + 超时自决规则,将 2PC 的单一不确定窗口拆分为两个可安全决策的窗口。
阶段 | 协调者行为 | 参与者行为 | 超时规则(核心) |
---|---|---|---|
1. CanCommit(预询问) | 向所有参与者发送是否可提交请求,等待反馈 | 仅自检(不占锁、不持久化关键日志),若可行返回 YES,否则返回NO | 协调者:超时未收齐YES → 宣告Abort 参与者超时未收到协调者后续指令 → 直接Abort(安全,因未占资源) |
2. PreCommit(预提交) | 仅当收齐所有YES时,发送预提交请求;否则发送Abort | 收到预提交后,持久化预提交完成日志(占锁、预留资源),返回ACK;收到Abort则直接回滚 | 协调者:超时未收齐ACK → 发送Abort 参与者超时未收到DoCommit指令 → 自主Commit(安全,因所有节点已通过预询问) |
3. DoCommit(正式提交) | 收齐预提交ACK后,发送正式提交请求;若中途发现异常(如参与者回滚),则发送Abort | 收到Commit则执行提交、释放资源、删除锁;收到Abort则回滚。返回ACK给协调者 |
非阻塞的本质在于两条超时自决规则:
3PC 通过阶段拆分,让参与者在任何超时场景下都有安全决策:
CanCommit 阶段超时:在 CanCommit 等不到协调者/指令,直接 ABORT
PreCommit 阶段超时:在 PreCommit 等不到DoCommit ,自主 COMMIT
这把 2PC 的唯一不确定窗口拆成两个:
PreCommit 之前超时 → 放弃(保证不会有人已提交)
PreCommit 之后超时 → 提交(保证大家都具备提交条件)
javascript
阶段1:CanCommit(预询问)
协调者 → 参与者:能提交吗?
参与者自检资源/逻辑,可行则回 YES,否则 NO
------超时策略:收不齐 YES => 协调者宣告 ABORT;参与者也可安全放弃
阶段2:PreCommit(预提交)
条件:收齐所有 YES
协调者 → 参与者:进入"可提交态",先落本地日志/锁资源,但还不写入最终提交;参与者回 ACK
------关键:一旦进入 PreCommit,大家都"随时可完成提交"
阶段3:DoCommit(正式提交)
协调者 → 参与者:执行提交;参与者提交并释放资源,回 ACK
4) 3PC 如何缓解 2PC 的典型故障场景
用三个场景对比:
场景 A:协调者在收齐 YES 之前宕机
2PC:有人已投 YES,有人还在等,不知道最终态,可能阻塞
3PC:仍处 CanCommit,参与者超时后安全 ABORT(没人进入 PreCommit)
场景 B:协调者在发出 PreCommit 之后、DoCommit 之前宕机
2PC:所有参与者都 prepare 完成,只能无限等最终决定
3PC:大家处于 PreCommit,等不到 DoCommit 就自主 COMMIT(非阻塞)
场景 C:协调者在发出 DoCommit 之后宕机
两者都能依赖持久化日志恢复到一致提交(已收到的提交照常提交,未收到的要么等恢复、要么依据恢复协议补齐)
协调者 DoCommit 后宕机,影响是没法收参与者的最终 ACK、没法统一处理异常,但不影响一致性,因为它已发完 Commit 指令:收到的参与者正常提交,没收到的参与者靠 PreCommit 日志(知道自己该提交),重启 / 超时后也会提交,最终结果一致
重启是因为参与者有可能在 PreCommit 阶段突然宕机,宕机后内存里的状态会丢,重启时只能靠日志找回之前已进入 PreCommit 的状态,才能按规则补提交;若没宕机,超时后直接自主 Commit 即可,不用重启。
5) 3PC 仍然不万能:网络分区下可能不一致
致命边界条件:分区导致有些参与者收到 PreCommit,另一些没收到
收到 PreCommit 的那一侧:按 3PC 规则,超时会 COMMIT
未收到的一侧:按规则会 ABORT
破坏原子性
这就是为什么 3PC 的非阻塞要靠同步/无分区假设;而在真实互联网环境(异步且可能分区),3PC 不能保证强一致
6) 形式化地看:状态机与超时转移
从工程实现角度,3PC 的非阻塞和一致性依赖状态机 + 持久化日志(WAL,Write-Ahead Log) ,确保节点重启后能恢复到正确状态。
参与者的核心状态流转:
Init(初始) → Waiting(CanCommit后,待预提交) → PreCommit(预提交完成) → Commit/Abort(最终态)
状态跃迁规则:
Waiting 状态:超时 / 收到 Abort → 跳转到 Abort;收到 PreCommit → 跳转到 PreCommit
PreCommit 状态:超时 / 收到 Commit → 跳转到 Commit;收到 Abort → 跳转到 Abort
任何状态跃迁前,必须先持久化日志,防止重启后状态丢失
日志的核心作用:幂等恢复
节点宕机重启后,通过读取 WAL 日志确定自身状态:
若日志记录已进入 PreCommit → 直接执行 Commit
若日志记录仅 Waiting → 执行 Abort
若日志记录已 Commit → 无需操作(确保幂等,避免重复提交)
幂等是多次执行同一操作,结果不变;恢复是崩溃后回到崩溃前的正确状态
核心结论
2PC 的痛点是阻塞:卡在 Prepare 后、Commit 前的不确定窗口,只能死等协调者,活性差但异步环境下安全性稳
3PC 的改进是拆窗自救:用 CanCommit+PreCommit 拆分窗口,让参与者能按状态超时自决(Waiting→Abort/PreCommit→Commit),缓解阻塞,但代价是多一轮通信、且分区下可能分叉,需依赖同步 / 部分同步网络
工业界少用 3PC,是因为它解决了 2PC 的阻塞,却引入了分区下的一致性风险,而 Raft/Paxos 等共识协议能在异步 + 分区环境下同时保证安全与活性,更适配复杂场景。
3PC的优缺点
优点:
比 2PC 更少阻塞。
引入超时机制,避免无限等待。
缺点:
实现复杂,消息开销大(多一轮通信)
仍不能完全解决一致性问题(极端网络分区时仍可能矛盾)
在实际工业界应用很少(数据库几乎不用 3PC)
3PC与 Paxos / Raft 对比
3PC:事务原子提交协议,目标是"分布式事务一致性"。
Paxos / Raft:分布式一致性协议,目标是"副本之间达成共识"。
区别:
3PC 偏向数据库事务。
Paxos / Raft 偏向分布式存储、日志复制。
工业界更常用 Raft / Paxos,而不是 3PC。
为啥工业界更爱 Raft/Paxos,不用 3PC?
因为 3PC 的本事太单一,只解决事务原子提交,
实际场景中,事务的需求可以用更灵活的方案(如 TCC、Saga)替代,不用死磕 3PC;
而副本共识(同步数据、选主)是所有分布式系统的基础刚需(比如分布式数据库、缓存集群、云服务都要),Raft/Paxos 刚好能稳定解决这个刚需,还能容忍网络分区、节点故障,比 3PC 更通用抗造。