两阶段提交(2PC)
两阶段提交协调多个资源对同一分布式事务作出统一的提交或中止决定。只有全部参与者投 YES,协调者才能决定 COMMIT;一个 NO 就要求整体 ABORT,包括先前投过 YES 的参与者。
可以把准备看成锁住资源、写下待定结果:YES 是接受后续决定的承诺,还没有应用新值。协调者若在此时失联,参与者不能仅凭自己的 YES 决定提交或中止,可能持锁等待。
准备、锁定与应用
画布是星形:协调者 C 分别联系 P1、P2、P3,参与者之间不通信。它们不是同一行的三个副本,而是不同资源管理器:P1 管账户 A,P2 管 B,P3 管审计日志。因此两票多数不能替代第三份资源的同意。
第一阶段,C 发送 PREPARE。参与者做本地检查,YES 记录准备状态、持锁,并把 proposed value 放在 current value 旁;NO 不锁定本例资源。C 要收齐 YES 才能提交,两票不够,三票本身也不是参与者已经应用。
第二阶段,C 作出 COMMIT 或 ABORT,把相同决定发给每个参与者。收到 COMMIT 才将待定值变为当前值并解锁;ABORT 丢弃待定值、保留原值并解锁。原子性指事务最终的决定一致,不表示所有机器在同一时刻更新画面;对外可见性还涉及隔离协议。
阻塞窗口从 YES 开始,到得知决定结束。参与者不能自行提交,因为其他人可能投 NO;也不能自行中止,因为 C 可能已决定 COMMIT。默认就停在 C 已 COMMIT、P1 仍 PREPARED、A 仍为 100 的边界。
逐步演示
All yes → commit 初值为 A=100、B=50、log=[]。事务 T 要从 A 扣 30、给 B 加 30、记录 T。关键边界在第 7、9、10、12 步。
- C 为 IDLE,P1–P3 为 WORKING。票数条是 C 的视图,第三格后标
need ALL YES。 - T 到达,C 变 WAITING,资源与锁未变。
- PREPARE 到 P1,P1 仍 WORKING。
- PREPARE 到 P2,尚无票。
- PREPARE 到 P3,问题已发出,答案尚未处理。
- P1 检查
100−30=70,投 YES、锁 A,紫色 proposed=70,current 仍 100。票数 1 YES/3。 - P2 准备 B 的 50→80 并锁定。2 YES/3 未越过门槛,C 仍等。
- P3 准备
[]→[T]并锁定,3 YES/3。 - C 决定 COMMIT。尚未有 COMMIT 消息发出,资源仍是待定状态。
- 展示阻塞窗口:C 知道决定,P1 不知道其他票与最终决定,A 仍 100,不能单方面结束准备。
- COMMIT 发往 P1,消息途中 P1 仍 PREPARED。
- P1 收到后应用,A=70、解锁、状态 COMMITTED,applied=1/3。
- COMMIT 发往 P2。
- P2 应用 B=80,applied=2/3。
- COMMIT 发往 P3。
- P3 应用 log=[T],applied=3/3。
- 结束为 A=70、B=80、log=[T]。先全体同意,再各自接收并应用。
One no → abort 把 A 改成 20,P1 余额不足投 NO、不持锁。P2、P3 仍投 YES 并准备,C 最终 ABORT。条带统计 resolved,不是 applied:所有人知道中止,但转账没发生。P2 丢弃 proposed 80,current 始终为 50,不是已经对外写成 80 后再改回。
Coordinator crash 复制默认流程直到 C 已决定 COMMIT,随后在任何 COMMIT 发出前失联。C 的日志视图不可访问,不表示真实耐久日志被删除。P1 只知道自己的 YES,没有决定;P1–P3 保持 PREPARED 与锁。预设不演示恢复,故以阻塞结束。
为什么要等待
Raft 的三比五复制同一日志,交叠多数与日志规则保留历史。2PC 的三资源分别承担事务的一部分,审计日志若拒绝,不能只让两个账户改变。它需要全部 YES。
可以把协调者决定放进 Raft 日志,降低单协调者故障影响,但复制 COMMIT 记录并不允许忽略投 NO 的资源管理器。共识复制的多数门槛不替代资源准备的全体门槛。
YES 之后,参与者已承诺接受最终决定,协调者迟到不等于可以撤销承诺。协调者可在尚未作出提交决定时因投票超时选择中止,但这不同于已准备参与者独自中止。预设省略超时处理、恢复日志、presumed abort、协作终止以及 3PC 等不同规则。
何时适用
事务跨独立资源且需要统一结果、能接受故障期间准备资源阻塞时,可采用 2PC。账户转移和审计记录是例子:不能把缺一份记录当成允许的部分成功。
若需求允许分步本地提交、失败后补偿,可考虑 saga;它提供的承诺不同,中间状态和补偿失败也需设计。若参与者只是同一日志的副本,则是复制共识问题,不应错套全资源投票。
本课消息量随 N 线性增长:每个参与者一条 PREPARE、一票、一条决定,YES 到决定之间持锁。准备和投票可并行,增加 N 增加消息、锁和慢参与者影响,不增加固定串行阶段数。画布固定 N=3;加第四个参与者仍要四个 YES。
边界情况
- 2 YES+1 NO,C 中止,两个 YES 参与者丢弃准备状态并解锁。
- A=100 可准备扣款,A=20 拒绝,不锁定该资源。
- decideCommit 后 A 仍为 100,决定与本地应用是不同事件。
- P1 已本地拒绝仍接收 ABORT,重复确认中止不会产生需要撤销的新改动。
- C 私下决定后崩溃,参与者不知道结果。自行提交在这条已全 YES 的轨迹中碰巧对,但在存在未知 NO 的另一条历史中会错,不能据事后视角制定规则。
常见错误
- 用两票多数提交。门槛写在第三格后,必须全体 YES。
- 把紫色 proposed 70 当已应用。current 从 100 变 70 发生在 applyCommit,不在投票时。
- 让 PREPARED 者因等待自行提交或中止,忽略未知决定。
- 假定画布参与者会互相选新协调者。这里没有横向消息和恢复协议;其他方案需额外定义。
- 把 Raft 与 2PC 的门槛互换。复制协调者也不改变全资源同意规则。
模型范围
一个 C、三个参与者、每步一条聚焦 RPC。省略决定 ACK、嵌套事务、投票超时、presumed-abort 优化、3PC、协作终止和协调者恢复,不展开耐久日志刷盘时序。崩溃预设不重放日志,留下阻塞队列。全体投票规则与准备后的不确定窗口是真实 2PC 性质,时间编排用于分清证据与应用。