想象你在筹备一场跨国婚宴:场地在巴厘岛、厨师从上海飞、摄影师来自巴黎、乐队从东京请。任何一方缺席,婚礼都无法完美。更麻烦的是,你不可能等开席之后才发现厨师没到——你需要在开席前,就让所有人拍着胸脯保证“我一定到”。 在软件世界,这个“总策划”叫两阶段提交(2PC)。它把一次跨多个数据库的操作拆成两轮:第一轮挨个问“能办到吗”,大家都说“能”,才进入第二轮——一声令下,同时执行。听上去很简单,但它背后藏着深刻的工程权衡:为了不出现“一半开席一半没来”的乱子,所有参与者都得先放下手头的活、占着资源傻等;万一总策划自己失联,整个婚礼就僵在原地;极端情况下甚至仍可能出乱子。这篇文章,就跟着这场婚宴,把这些门道一层层拆开看。

岗位关联

对后端与数据方向的 AI 从业者而言,2PC 是理解分布式一致性的第一块基石——读懂它的原理与代价,才能在架构选型和故障排查中做出靠谱决策。

目录
  1. 可视化演示
  2. 分布式事务背景
  3. 两阶段提交流程
  4. 协议角色职责
  5. 2PC核心优势
  6. 2PC痛点分析
  7. 3PC改进方案
  8. TCC柔性事务
  9. 工程选型考量
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态演示分布式事务2PC的核心机制,通过基础/核心/应用/扩展四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

分布式事务2PC:PC协议 2PC协议的协调者与参与者角色 Prepare准备阶段的执行流 核心机制可视化 分布式事务2PC整体架构与关键组件

分布式事务背景

筹备跨国婚宴时你会发现,本地“家务事”好办——买菜、订桌、找司仪,一个管家就能搞定。可一旦涉及场地、厨师、摄影师、乐队这些“外部供应商”,每一家都有自己的日程和标准:场地可能同时接了别的活,厨师可能临时改航班。你没法用“家里的一套规矩”要求他们,只能靠反复沟通、统一约定。

软件里的“家务事”就是本地事务:在一个数据库内,事务的 ACID(原子性、一致性、隔离性、持久性)由数据库引擎保证,要么全成功、要么全失败。但当业务数据分散在多个数据库——比如订单在订单库、库存在库存库、余额在账户库——一次操作要同时改动三个库,本地事务就“各管各的”了。这就是分布式事务要解决的问题:如何让跨库的多步操作像本地事务一样原子。

分布式系统有个著名的 CAP 定理:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)三者最多同时满足两个。网络分区(P)在现实中无法避免,所以真正的选择发生在 C 和 A 之间。2PC 是“CP 派”的代表:它宁可让系统在提交期间不可用,也要保证所有节点最终状态一致。

就像为了婚宴不出岔子,总策划宁可让所有供应商在开席前“原地待命”,也不允许出现“这边开席、那边没来”的情况——这就是用一小段等待,换一个确定的结局。

CAP 定理:C+A+P≤2\text{CAP 定理}:\quad C + A + P \le 2
这个式子是说:一致性、可用性、分区容错性三个目标,最多只能同时实现两个。因为网络总会出问题,分布式系统通常必须先接受 P,剩下的选择就变成“要 C 还是要 A”——2PC 选择了 C。
  1. C(一致性)所有节点在同一时刻读到相同的数据,相当于婚宴上每桌的菜必须同时上齐、内容一致。
    C=一致性C = \text{一致性}
  2. A(可用性)系统始终能及时响应请求,相当于婚宴必须按计划开席,不能无限期推迟。
    A=可用性A = \text{可用性}
  3. P(分区容错性)即使部分节点之间网络断裂,系统也要继续工作,相当于个别供应商失联时婚宴不能直接停摆。
    P=分区容错性P = \text{分区容错性}
  4. ≤ 2(三选二)三个目标只能同时满足两个;分布式系统必须接受 P,剩下的取舍就在 C 和 A 之间。
    固定 P, 再在 C, A 中取舍\text{固定 } P,\ \text{再在 } C,\ A \text{ 中取舍}

两阶段提交流程

总策划定下规矩:所有供应商必须经历两轮确认。第一轮是“探底”——逐个打电话:“这周六的食材能备齐吗?摄影档期能留吗?乐队能到场吗?”每个供应商收到询问后,开始准备并锁定档期,然后回话:“能”或“不能”。注意,这时他们只是“待命”,还没有真正开工。

这就是两阶段提交(2PC)的“准备阶段”(Prepare Phase):协调者向所有参与者发送 prepare 请求;每个参与者执行本地事务到“可提交”状态——写入 undo/redo 日志、对涉及的数据加锁——然后向协调者回复 YES(同意提交)或 NO(无法提交)。参与者绝不能在这时提前提交,因为最终决定权还在协调者手里。

当协调者收齐全部投票,进入“提交阶段”(Commit Phase):如果所有投票都是 YES,协调者广播 commit 指令,各参与者正式提交并释放锁;如果存在任意一个 NO,协调者广播 abort 指令,各参与者利用 undo 日志回滚到事务开始前。两个阶段的语义可以概括为:先确认“每个人都办得到”,再执行“所有人都同时办”。

回到婚宴:先确认“每家都能来”,再宣布“开席”。这样一来,“10 桌开席、2 桌没上菜”的场面被从流程上杜绝了——要么全场照常,要么全场取消,不存在中间态。

Commit  ⟺  ⋀i=1nVotei=YES;∃ Votei=NO⇒Abort\text{Commit} \iff \bigwedge_{i=1}^{n} Vote_{i} = \text{YES};\qquad \exists\, Vote_{i} = \text{NO} \Rightarrow \text{Abort}
提交的充要条件是“所有投票都是 YES”;只要存在一个 NO,整体就进入中止分支。这正是 2PC 把“全员通过”变成硬性门槛的数学表达。
  1. Vote_i第 i 个参与者的投票,取值只有 YES 或 NO,就像供应商只能回答“能”或“不能”。
    Votei∈{YES, NO}Vote_{i} \in \{\text{YES},\ \text{NO}\}
  2. ∧(逻辑与/全称)对所有参与者的投票做“与”运算,任何一个不是 YES,整体就不成立,即要求每个 i 都投 YES。
    ⋀i=1nVotei=YES  ⟺  ∀i:Votei=YES\bigwedge_{i=1}^{n} Vote_{i} = \text{YES} \iff \forall i: Vote_{i} = \text{YES}
  3. ∃(存在)只要存在一个 NO,协调者就触发中止分支,广播 Abort,不提交任何节点。
    ∃ NO⇒Abort\exists\, NO \Rightarrow \text{Abort}
  4. 整体含义最终结果只有“全提交”或“全回滚”两种,这就是“全有或全无”的原子性来源。
    Outcome∈{全提交, 全回滚}\text{Outcome} \in \{\text{全提交},\ \text{全回滚}\}

协议角色职责

婚宴筹备里,总策划和供应商的分工很清晰:策划师不亲自炒菜、不亲自摄影,他只负责三件事——发出询问、收集回复、下达最终指令;而每个供应商只对自己的环节负责,接到指令就执行,没接到指令就待命。

2PC 同样只有两类角色:协调者(Coordinator)与参与者(Participant)。协调者通常位于发起事务的应用节点或专门的事务管理器上,是整个协议的大脑;参与者是各资源管理器(数据库、消息队列等),负责执行本地事务。协调者与参与者之间通过标准化的 prepare / commit / abort 消息交互,协议本身不关心业务逻辑。

参与者的本地状态是一个有限状态机:初始为 WORKING(事务执行中),prepare 成功后进入 PREPARED(已锁定资源、已写日志),最终根据指令到达 COMMITTED 或 ABORTED。协调者也要持久化自己的决策日志(比如“已发送 commit”),否则它宕机恢复后会忘记自己推进到哪一步,导致无法恢复现场。

这就像每家供应商虽然各有各的厨房和流程,但都必须遵守策划师的“状态协议”:备料中、就绪待命、开工、停工。谁也不能绕过这些状态自作主张——这正是 2PC 能把多方行为“对齐”到同一节奏上的根本原因。

Statei∈{WORKING, PREPARED, COMMITTED, ABORTED}State_{i} \in \{\text{WORKING},\ \text{PREPARED},\ \text{COMMITTED},\ \text{ABORTED}\}
每个参与者只能在四个状态之间切换:执行中、已准备、已提交、已中止。状态机约束保证了参与者不可能“跳过准备直接提交”或者“提交后又反悔”。
  1. State_i参与者 i 在任意时刻所处的本地事务状态。
    Statei:第 i 个参与者的状态State_{i} : \text{第 } i \text{ 个参与者的状态}
  2. WORKING事务正在本地执行,变更尚未定型,相当于厨师正在备菜。
    WORKING→执行中\text{WORKING} \rightarrow \text{执行中}
  3. PREPARED已写日志、已锁资源,等待协调者的最终指令,相当于菜已备好、灶台被占住。
    PREPARED→待命\text{PREPARED} \rightarrow \text{待命}
  4. COMMITTED / ABORTED收到最终指令后的两种归宿——要么开席,要么取消。
    PREPARED→{COMMITTED, ABORTED}\text{PREPARED} \rightarrow \{\text{COMMITTED},\ \text{ABORTED}\}

2PC核心优势

策划师这套“两轮确认”虽然繁琐,但换来的是所有人都最看重的东西——确定性:要么全场开席,要么全场取消,绝不存在“主桌开席、乐队没来”的闹剧。对于婚宴这种不容闪失的大事,确定性比效率更值钱。

2PC 的核心价值就是强一致性。在提交阶段,所有参与者的提交指令由同一个协调者发出,配合准备阶段对资源的全局锁定,可以严格保证事务的原子性(Atomicity)——全有或全无,没有任何中间状态暴露给外部。

从工程角度看,2PC 的优点还有:实现相对简单,业务代码不需要写补偿逻辑,只需处理正常提交/回滚两个分支;同时它被 XA 规范采用,成为事实标准,MySQL、Oracle、PostgreSQL 等主流数据库都内置支持,可以用最少的定制代码接入。这也是为什么很多金融级系统至今仍用 2PC 处理核心账务。

一句话:如果你需要的是一份“同一时刻、全体一致”的硬承诺,2PC 是历史上最成熟、最不需要业务方操心的方案——婚宴图省心,就找总策划全权指挥。

⋀i=1ntxi=Commit∨⋀i=1ntxi=Abort\bigwedge_{i=1}^{n} tx_{i} = \text{Commit}\quad \lor\quad \bigwedge_{i=1}^{n} tx_{i} = \text{Abort}
要么所有本地事务全部提交,要么全部回滚,两个结果互斥且必居其一。这个“或”字就是原子性:事务像原子一样不可分割。
  1. tx_i参与者 i 上的本地事务,相当于每个供应商手头的“那么活儿”。
    txi:节点 i 的本地事务tx_{i} : \text{节点 } i \text{ 的本地事务}
  2. 全体 Commit所有参与者的事务全部成功提交,没有一个落下。
    ⋀i=1ntxi=Commit  ⟺  全员提交\bigwedge_{i=1}^{n} tx_{i} = \text{Commit} \iff \text{全员提交}
  3. 全体 Abort所有参与者的事务全部回滚,协议禁止“部分提交”的中间状态。
    ⋀i=1ntxi=Abort  ⟺  全员回滚\bigwedge_{i=1}^{n} tx_{i} = \text{Abort} \iff \text{全员回滚}
  4. ∨(二选一)两个结果互斥且必居其一,这就是业务代码只需处理两个分支的原因。
    Outcome=全提交∨全回滚, 无中间态\text{Outcome} = \text{全提交} \lor \text{全回滚},\ \text{无中间态}

2PC痛点分析

策划师的两轮确认有个致命软肋。当厨师备好菜、占着灶台等待“开席”指令时,他推掉了其他订单;如果策划师宣布完“准备开席”后手机突然没电,所有供应商都不知道该不该开工——全场傻等。更糟的是,策划师要是永远联系不上,这场婚宴就永远卡在半路。

这就是 2PC 最著名的痛点——同步阻塞。准备阶段锁定资源后,参与者必须一直持有锁直到收到最终指令,期间其他事务无法访问这些数据。参与者越多、单点越慢,整体等待时间越长,事务吞吐随之大幅下降。

第二个痛点是单点故障:协调者是唯一的决策者,它在提交阶段宕机,所有参与者都无法自行决定提交还是回滚,只能阻塞等待其恢复;这期间系统处于“半瘫”状态。第三个痛点是脑裂与数据不一致:如果协调者在发送 commit 给一部分参与者后宕机,先收到 commit 的参与者已提交,其余参与者还停在 PREPARED——两边状态不一致,必须靠人工核对或额外的恢复协议补救。

所以说,2PC 的可靠性很大程度押在协调者这一个人身上——总策划不能生病、不能失联,甚至不能“漏发指令”。这正是它让架构师又爱又怕的原因。

P(至少一个参与者故障)=1−∏i=1n(1−pi)≈n⋅pˉP(\text{至少一个参与者故障}) = 1 - \prod_{i=1}^{n} (1 - p_{i}) \approx n \cdot \bar{p}
这个公式告诉你:参与者越多,整体出岔子的概率越大。如果每家供应商有 1% 的“放鸽子”概率,那 30 家供应商的婚宴就有约 26% 的概率至少一家掉链子——2PC 对节点数量非常敏感。
  1. p_i每个参与者出错的概率,相当于每家供应商“放鸽子”的概率。
    pi:参与者 i 的故障概率p_{i} : \text{参与者 } i \text{ 的故障概率}
  2. ∏(1-p_i)所有参与者同时正常工作的概率,供应商越多,乘积越小。
    ∏i=1n(1−pi)=P(所有人都正常)\prod_{i=1}^{n} (1-p_{i}) = P(\text{所有人都正常})
  3. 1 - ∏用 1 减去“全部正常”的概率,得到“至少一个人掉链子”的概率。
    1−∏i=1n(1−pi)=P(至少一人故障)1 - \prod_{i=1}^{n} (1-p_{i}) = P(\text{至少一人故障})
  4. ≈ n·p̄当每个节点的故障概率为 p 时,整体失败概率大约是 n 倍的 p,随节点数线性上升。
    Pfail≈n⋅pˉ⇒分支数不宜过多P_{\text{fail}} \approx n \cdot \bar{p} \Rightarrow \text{分支数不宜过多}

3PC改进方案

为了救“策划师失联”的场,有人发明了 3PC(三阶段提交)。它给婚宴加了一道“预签到”:第一轮,策划师先打电话问“你们确定愿意来吗?愿意我们才往下走”;得到全员确认后,第二轮才让大家开始备料、锁定档期;第三轮才正式宣布开席。同时,每家供应商人手一份应急手册:等不到指令超过一定时间,就自行取消。

3PC 在 2PC 中间插入 CanCommit 阶段,把原来的 prepare 拆成了 canCommit(询问是否有能力提交)和 preCommit(预提交:写日志、锁资源、回复 ready)两步。最关键的是引入了超时机制:参与者在 preCommit 之后若迟迟收不到 doCommit,会主动中止,而不是像 2PC 那样无限阻塞。

完整流程是:①CanCommit——协调者询问“能否提交”,全员 YES 才继续;②PreCommit——协调者广播预提交,参与者写日志、锁资源并确认;③DoCommit——协调者广播最终提交,参与者正式提交。若协调者在 preCommit 后宕机,参与者等待超时后按“多数派规则”自行决定继续或中止,尽量恢复可用。

3PC 用“多一轮沟通”换来了“不怕失联”,但它并没有完美收官:多数派决定中止时,少数已提交的节点仍会不一致,只是概率和影响窗口被缩小了。婚宴上多加一轮确认降低风险,但隐患并没有从根上消失。

3PC: CanCommit→PreCommit→DoCommit,超时⇒多数派兜底\text{3PC}:\ CanCommit \rightarrow PreCommit \rightarrow DoCommit,\quad \text{超时} \Rightarrow \text{多数派兜底}
3PC 把流程从两轮改成三轮,并给参与者戴上“超时手表”:等不到指令就按多数派来决定,不再傻等协调者。代价是正常路径上多了一轮网络开销。
  1. CanCommit第一轮只询问“能力”,不锁任何资源,相当于先问“这周六能来吗”,成本为零。
    CanCommit:能力询问,零成本CanCommit: \text{能力询问,零成本}
  2. PreCommit第二轮写日志、锁资源并到达“预提交”状态,相当于备好菜、原地待命。
    PreCommit:预提交,等待最终裁决PreCommit: \text{预提交,等待最终裁决}
  3. DoCommit第三轮正式提交,相当于宣布开席。
    DoCommit:正式提交DoCommit: \text{正式提交}
  4. 超时 ⇒ 多数派兜底参与者等太久就按多数派规则自主决定,不再无限阻塞,这是 3PC 最重要的改进。
    timeout⇒参与者按多数派自主决定\text{timeout} \Rightarrow \text{参与者按多数派自主决定}

TCC柔性事务

如果婚宴实在等不起“全场同步”,还可以换一种思路:订金制。每家供应商先收订金、锁住档期(这叫预留);到了日子确认支付尾款、正式开工(这叫确认);如果有人临时反悔,退还订金、取消订单(这叫冲正)。三家不一定在同一秒完成,但最后账目一定是平的。

这个模式在软件里叫 TCC(Try-Confirm-Cancel),是一种业务层的柔性事务方案。它的三个阶段是:Try——只做资源预留和业务校验,不真正改动业务数据,比如冻结账户金额、预占库存;Confirm——确认业务真正生效,比如扣款、扣库存;Cancel——冲正 Try 的预留,比如解冻金额、释放库存。

TCC 没有全局锁、没有同步阻塞,每个参与者独立提交本地事务,靠业务侧的状态机和补偿操作收敛到最终一致——属于 AP 派方案。代价是:每个业务操作都要额外编写 Try/Confirm/Cancel 三套逻辑,还要保证幂等(Confirm 和 Cancel 可能被重试);开发成本和无 Bug 难度明显高于 2PC。业界常用的 Seata TCC、TCC-Transaction 都是这个思路的落地框架。

如果说 2PC 是“总策划全权指挥”,TCC 就是“各家自律 + 事后对账”:牺牲了一点实时强一致,换来了不会因为一个人卡死全局的弹性。具体选谁,取决于你对“同时”有多较真,对“晚一点平账”有多宽容。

TCC: Try⏟预留资源→Confirm⏟确认执行∨Cancel⏟冲正回滚TCC:\ \underbrace{Try}_{\text{预留资源}} \rightarrow \underbrace{Confirm}_{\text{确认执行}} \lor \underbrace{Cancel}_{\text{冲正回滚}}
TCC 把一次业务操作拆成“预留 → 确认或冲正”。每个参与者最终要么 Confirm、要么 Cancel,两者必居其一,配合幂等重试就能收敛到最终一致。
  1. Try(预留)只做校验和占位,不改变最终业务数据,相当于收订金、锁档期。
    Try:预留资源⇏业务生效Try: \text{预留资源} \not\Rightarrow \text{业务生效}
  2. Confirm(确认)业务真正生效,相当于全款到账、正式开工。
    Confirm:预留⇒生效Confirm: \text{预留} \Rightarrow \text{生效}
  3. Cancel(冲正)撤销 Try 的预留,相当于退还订金、取消订单。
    Cancel:预留⇒∅Cancel: \text{预留} \Rightarrow \varnothing
  4. ∨(最终二选一)每个参与者在 Confirm 与 Cancel 中必居其一,最终账目收敛一致,但不是同一秒强一致。
    ∀i: Confirmi∨Canceli, 最终收敛\forall i:\ Confirm_{i} \lor Cancel_{i},\ \text{最终收敛}

工程选型考量

回到婚宴本身:如果客人只有一桌,根本不需要跨国供应商,本地办一场就完事——单库事务足够;如果非跨国不可,又不能容忍任何闪失,就接受“全场等待”的 2PC;如果怕一个人卡死全局,就用 3PC;如果想保持高吞吐、也愿意写补偿代码,就上 TCC。没有最好的方案,只有最合适的选择。

工程选型的本质,是在一致性强度、性能开销、开发复杂度三者之间权衡:2PC 适合强一致性要求极高、跨库分支少(通常不超过 5 个)、并发量可控的核心链路,比如账户扣款、库存预占;TCC、本地消息表、事务消息适合允许最终一致、又需要较高吞吐的场景,比如订单、积分、通知类业务。

具体到业界实践:数据库 XA 协议(基于 2PC)在高并发下锁等待时间长,已经越来越少见;更常见的组合是 Seata 的 AT 模式(对 2PC 的改良,自动回滚)、TCC 框架、RocketMQ 事务消息,以及最朴素的“本地事务 + 异步对账”。无论选哪种,幂等设计、重试机制、日志落盘、对账任务都是绕不开的工程底座。

一句话总结:分布式事务没有银弹。你选择的不只是一个协议,更是一整套关于“等多久、谁负责、出错了怎么赔”的承诺书——理解了 2PC,就理解了为什么这些承诺书每一本都写着代价。

T2PC 单事务≈2×RTT+2×Tfsync,P整体失败∝nT_{\text{2PC 单事务}} \approx 2 \times RTT + 2 \times T_{fsync},\qquad P_{\text{整体失败}} \propto n
2PC 单次事务的关键路径上约有两轮网络往返和两次磁盘落盘,而且参与者越多、失败概率越高。所以工程上要控制跨库分支数,并在强一致和性能之间做出取舍。
  1. 2 × RTT两轮网络往返(准备请求+投票、提交指令+确认),每一轮都占用事务关键路径。
    2×RTT:准备轮 + 提交轮2 \times RTT: \text{准备轮 + 提交轮}
  2. 2 × T_fsyncprepare 日志和 commit 日志各一次强制刷盘,保证故障后可恢复。
    2×Tfsync:两次磁盘落盘2 \times T_{fsync}: \text{两次磁盘落盘}
  3. P ∝ n参与者数量 n 越大,任一出错导致整体回滚的概率越高,所以 2PC 不适合大规模分片。
    P整体失败∝n⇒控制分支数≤5P_{\text{整体失败}} \propto n \Rightarrow \text{控制分支数} \le 5

AI 应用场景

跨行转账与核心账务一致性

阶段:数据链路一致性保障(资金数据层面)

解决的问题:传统单库事务只能保证一个库内原子,跨行或跨库的“扣款+入账”一旦第二步失败,就会出现资金差账;AI 风控系统的对账任务每天要花大量人力修复这些长短款。2PC 让扣款库与流水库在同一事务中同时提交或同时回滚,从源头消除差账。

具体案例:某支付平台的核心账务系统,使用基于 XA(2PC 的数据库实现)的分布式事务管理用户账户库与交易流水库。用户发起转账时,两个库先进入准备阶段(写日志、锁账户行),全员通过后统一提交;某次网络抖动导致流水库准备超时,账户库自动回滚,资金守恒由日志和日终对账双重兜底。

电商订单与库存的强一致扣减

阶段:在线业务链路一致性保障(订单/库存数据)

解决的问题:订单库与库存库分离后,若先扣库存后下单失败,会产生“有单无货”;若先下单后扣库存,又可能超卖。2PC 将“下单成功”与“库存扣减”绑定为同一个原子操作。

具体案例:某电商平台的限量商品购买链路,订单中心和库存中心通过 Seata AT 模式(2PC 的改良实现)协同:下单事务同时对两个库加锁并准备,全部通过后统一提交;库存不足时整体回滚,超卖率降为零。大促场景下,同一团队将热点商品的库存扣减改为 TCC 预留,既保证不超卖又提升了吞吐。

Python 演示

python87 行
# -*- coding: utf-8 -*-"""两阶段提交(2PC)最小模拟: 协调者 + 两个参与者(银行账户)""" class Participant:    """参与者: 一个数据库节点, 管理一个账户余额"""     def __init__(self, name, balance):        self.name = name        self.balance = balance        self.frozen = 0      # 已冻结(预留)的金额        self.pending = None  # 待生效的金额变更     def prepare(self, delta):        """准备阶段: 校验余额并冻结金额, 返回是否同意提交"""        if self.balance + self.frozen + delta < 0:            return False        self.pending = delta        self.frozen += delta        return True     def commit(self):        """提交阶段: 让冻结的变更正式生效"""        if self.pending is None:            return        self.balance += self.pending        self.frozen -= self.pending        self.pending = None     def rollback(self):        """中止阶段: 释放冻结, 回到 prepare 前的状态"""        if self.pending is None:            return        self.frozen -= self.pending        self.pending = None  class Coordinator:    """协调者: 执行两阶段提交协议"""     def __init__(self, participants):        self.participants = participants     def two_phase_commit(self, deltas):        """deltas: 每个参与者对应的金额变更(如 -200 / +200)"""        # 阶段一: 准备阶段, 向全体征询        print("== 阶段一: 准备(Prepare) ==")        for p, d in zip(self.participants, deltas):            ok = p.prepare(d)            print("  {} 投票: {}".format(p.name, "YES" if ok else "NO"))            if not ok:                print("  存在NO票, 发起中止(Abort)")                for q in self.participants:                    q.rollback()                return False        # 阶段二: 提交阶段, 全员通过则统一提交        print("== 阶段二: 提交(Commit) ==")        for p in self.participants:            p.commit()            print("  {} 已提交".format(p.name))        return True  def show(participants):    total = 0    for p in participants:        total += p.balance        print("    {} 余额 = {}".format(p.name, p.balance))    print("    合计 = {} (守恒校验)".format(total))  def main():    alice = Participant("账户A", 500)    bob = Participant("账户B", 300)    coord = Coordinator([alice, bob])    print("转账 200: A -> B")    ok = coord.two_phase_commit([-200, 200])    print("结果:", "成功" if ok else "回滚")    show([alice, bob])    print("---")    print("再转账 800: A -> B (余额不足, 应整体回滚)")    ok = coord.two_phase_commit([-800, 800])    print("结果:", "成功" if ok else "回滚")    show([alice, bob])  if __name__ == "__main__":    main()
目的:用不到 80 行模拟 2PC 的协调者与参与者,演示“全员通过→提交”和“余额不足→整体回滚”两个场景,直观看到锁资源、统一提交、广播回滚的过程。
  1. def prepare(self, delta):参与者完成准备阶段:校验余额并冻结金额,返回是否同意提交。
  2. if self.balance + self.frozen + delta < 0:余额不足直接投 NO 票,这是触发整体回滚的唯一入口。
  3. self.pending = delta只记下待生效变更,不立即改余额,模拟 prepare 阶段“只预备、不生效”。
  4. for p, d in zip(self.participants, deltas):协调者逐个征询参与者,严格按协议顺序推进。
  5. if not ok:只要有一个 NO,立即终止准备阶段,不再询问后面的参与者。
  6. q.rollback()广播回滚,让所有已 PREPARED 的参与者释放冻结资源。
  7. p.commit()全员通过后统一提交,保证“全有或全无”。
  8. if __name__ == "__main__":脚本可直接运行,演示转账成功与余额不足回滚两种结局。

常见误区

误区我学完 2PC 觉得它很完美:准备阶段问一遍、提交阶段通知一遍,数据不就永远一致了吗?协调者出点小问题应该也能自动恢复吧?

正解2PC 只在“无故障”或“故障可恢复”的前提下保证原子性。如果协调者在提交阶段宕机,一部分参与者已收到 commit、另一部分没有,两边就会出现不一致,且协议本身无法自动修复,必须靠人工核对或额外的恢复机制。它更像婚宴上的总策划:策划师在宣布“开席”的瞬间倒下,就一定会有人开席、有人没开。

为何常见:教材和文章通常先讲“正常流程”,把最棘手的协调者宕机场景放在最后,初学者容易把“设计目标”误当成“铁定保证”。

误区我以为 prepare 成功就等于本地事务已经提交了,那锁自然可以释放掉,等协调者让提交的时候再处理一下就行。

正解prepare 成功只是“预备”,最终状态还没定。参与者必须一直持有锁并等待协调者的最终指令,期间不能释放,否则其他事务可能读到未定型的半成品数据,破坏隔离性和原子性。这就是 2PC 同步阻塞的根源——厨师备好菜,但锅和灶台必须一直占着。

为何常见:把 prepare 类比成“本地提交”是最大的误解来源,实际上 prepare 只是“承诺可以提交”。

误区3PC 又有询问阶段又有超时机制,参与者不会再傻等了,那它应该完全替代 2PC,数据也绝对不会不一致了。

正解3PC 缩小了“等待窗口”和“不一致窗口”,但并没有消灭它们。协调者在 preCommit 后宕机,参与者超时后按多数派自行决定,仍可能与少数已提交的节点不一致;只是概率更低、恢复更快。它还把正常路径的延迟从两轮增加到了三轮,并非免费午餐。

为何常见:看到“改进”两个字就默认是“完美版”,忽略了分布式系统的根本限制——网络分区下,任何协议都做不到绝对一致和高可用兼得。

误区TCC 也是 Try、也是 Confirm/Cancel,跟 2PC 的准备、提交不是一回事吗?我写 TCC 的时候把它当成 2PC 来用。

正解两者本质不同:2PC 是数据库协议层的强一致方案,有全局锁、有同步阻塞,参与者被动执行;TCC 是业务层的柔性事务,每个操作独立提交本地事务,靠 Try 预留、Confirm 执行、Cancel 冲正,配合幂等重试达到最终一致。TCC 没有全局锁,但也因此失去了实时强一致。

为何常见:两者都有“先准备、后决定”的外形,容易让人只看表面流程,忽略一致性模型的根本差异。

面试题

Q1. 两阶段提交(2PC)的第一个阶段叫什么?

单选题 · easy

  • A.准备阶段← 正确答案
  • B.提交阶段
  • C.预提交阶段
  • D.回滚阶段

答案:A。2PC 的第一个阶段是准备阶段(Prepare),协调者向参与者征询“能否提交”;第二个阶段才是提交或回滚。预提交阶段是 3PC 的概念,属于干扰项。

Q2. 在 2PC 的准备阶段,参与者需要完成哪些工作?

单选题 · mid

  • A.直接提交本地事务即可
  • B.执行事务写日志并锁资源← 正确答案
  • C.释放所有锁并返回投票
  • D.等待协调者发送准备请求

答案:B。准备阶段参与者要把事务执行到可提交状态,写 undo/redo 日志并锁住资源,之后才能回复 YES/NO。“直接提交”和“释放锁”都会破坏原子性,而“等待准备请求”是协调者的职责,不是参与者的动作。

Q3. 当 2PC 的协调者收到全部参与者的 YES 投票时,会执行什么操作?

单选题 · easy

  • A.向所有参与者广播中止
  • B.向所有参与者广播提交← 正确答案
  • C.忽略掉所有参与者的投票
  • D.重新发起一个新的准备阶段

答案:B。全员 YES 是提交的唯一充要条件,协调者就此广播 Commit。只要存在一个 NO 就会广播 Abort;忽略投票和重新准备都不在协议流程中。就像婚宴上所有供应商都确认能到,总策划才宣布开席。

Q4. 以下哪一项是 2PC 最典型的工程痛点?

单选题 · mid

  • A.实现过于复杂难以落地
  • B.同步阻塞与协调者单点故障← 正确答案
  • C.无法保证事务的原子性
  • D.只能应用在单机数据库

答案:B。2PC 准备阶段要持锁等待、协调者宕机则全员卡死,这两点分别对应同步阻塞与单点故障。它实现简单(A 错)、原子性有保障(C 错)、也能跨数据库使用(D 错)。

Q5. 3PC 相比 2PC 最主要的改进是什么?

单选题 · mid

  • A.减少了网络交互的总轮次
  • B.引入询问阶段与超时兜底← 正确答案
  • C.彻底去掉了协调者角色
  • D.取消所有阶段的资源锁定

答案:B。3PC 增加 CanCommit 询问阶段,并给参与者设置超时兜底,缩小阻塞窗口。它轮次更多而非更少(A 错)、仍需要协调者(C 错)、preCommit 阶段依然要锁资源(D 错)。

Q6. 在经典 2PC 中,协调者在提交阶段宕机,已处于 PREPARED 的参与者会?

单选题 · mid

  • A.立即自行提交本地事务
  • B.立即自行回滚本地事务
  • C.阻塞等待协调者恢复← 正确答案
  • D.与其它参与者协商后处理

答案:C。经典 2PC 的参与者无法区分“协调者已发 commit 但我没收到”和“协调者还没发 commit”两种情况,只能阻塞等待。A 和 B 都可能破坏一致性,D 在 2PC 中不存在节点间的协商机制。

Q7. 关于 2PC 与 TCC 的对比,下列说法正确的是?

单选题 · hard

  • A.两者都属于数据库协议层方案
  • B.TCC 没有全局锁但需业务补偿← 正确答案
  • C.2PC 的全局锁不会影响吞吐
  • D.TCC 能提供实时的强一致性

答案:B。TCC 是业务层方案(A 错),Try 只做资源预留、无全局锁,并发更好,但要写 Try/Confirm/Cancel 三套逻辑并保证幂等。2PC 持锁时间长、吞吐低(C 错);TCC 是最终一致而非实时强一致(D 错)。

Q8. 2PC 在哪些异常场景下可能无法保证强一致?

多选题 · hard

  • A.协调者在提交阶段宕机← 正确答案
  • B.网络分区使部分节点失联← 正确答案
  • C.参与者准备阶段投票反对
  • D.所有节点正常且网络稳定

答案:AB。协调者宕机或网络分区会让部分节点收到 commit、部分收不到,从而卡在中间状态,可能不一致。C 会触发整体回滚,是协议内的正常路径;D 是正常场景,能保证强一致。

Q9. 下列哪些是 2PC 参与者的合法状态?

多选题 · easy

  • A.正在执行工作处理← 正确答案
  • B.已准备就绪待命← 正确答案
  • C.已提交或已中止← 正确答案
  • D.等待投票决定

答案:ABC。参与者状态机为 WORKING → PREPARED → COMMITTED/ABORTED。“等待投票决定”不是协议中的正式状态,参与者的投票行为发生在进入 PREPARED 之前,属于干扰项。

Q10. 关于分布式事务方案的选择,下列说法正确的是?

多选题 · mid

  • A.两阶段提交属于强一致性方案← 正确答案
  • B.TCC 属于最终一致性方案← 正确答案
  • C.最终一致可选用TCC或消息← 正确答案
  • D.三阶段提交可以彻底解决分歧

答案:ABC。2PC 是强一致、TCC 是最终一致;允许最终一致时优先选 TCC、本地消息表或事务消息以换取吞吐。3PC 只是缩小不一致窗口,并不能彻底解决网络分区下的脑裂问题,D 错。