
刚接触分布式事务那会儿我也觉得2pc和3pc的比较就是一道“背诵题”一个两阶段一个三阶段多一个阶段自然更可靠。直到我亲手负责一个同时写订单库和库存库的接口第一次遇到协调者进程异常退出才意识到这场比较根本不是“谁的阶段多谁就先进”而是你要在一致性、可用性和网络现实之间做取舍。这篇文章我想用项目里踩过的坑加上后来补上的理论复盘把2PC与3PC的完整机制、故障模型以及现代系统里它们各自的真实去留一次讲清楚。如果你正被分布式事务的一致性方案困扰或者准备面试架构岗这篇应该能帮你少走不少弯路。1. 为什么要关心2PC和3PC先认识分布式原子提交问题1.1 单机事务的“保护罩”在分布式场景下失效我们先从最熟悉的东西说起单个数据库里的本地事务。你用BEGIN开启事务执行几条SQL最后COMMIT或者ROLLBACK底层有redo log、undo log、锁机制在配合工作。崩溃了怎么办恢复的时候靠日志回放要么把已提交的变更补上要么把未提交的变更回滚一切都在一个存储引擎内部完成原子性由日志和锁机制共同保证。但业务一旦拆分事情就完全变了。订单库在A机器库存库在B机器账户库在C机器。一个下单接口要同时写这三个库本地事务只能保证“我自己的库要么提交要么回滚”没有任何办法知道其他库提交了没有。假设订单库提交成功了库存库在提交前宕机了系统的最终状态就是“订单已创建库存没扣”。这就是教科书里说的分布式原子提交问题多个独立节点上的子事务如何做到全局要么全部提交、要么全部回滚并且绝不允许出现部分提交。你可能会想能不能让每个库先把操作做完最后各自决定不行。因为事务是并发的当你看到库存库失败的时候订单库可能已经把数据暴露给其他读请求了。要让这个状态可控制、可恢复必须引入一个“裁判”角色负责收集所有节点的意愿然后给出一个全局唯一的最终决定。1.2 协调者思路把独立节点聚到同一张决策桌上这个“裁判”就是事务协调者TMTransaction Manager。它的工作方式很朴素先问所有人“这个事务你们能不能提交”收集完所有答复后拍板“提交”或“回滚”再让所有人执行同一个决定。听起来简单实际设计起来有很多隐藏问题。协调者问完了突然自己挂了怎么办参与者已经准备好的人是提交还是回滚等了很久没收到协调者的指令是继续等还是自行决定这些问题没有一个正确答案取决于你愿意牺牲什么是牺牲可用性换来绝对的一致性还是牺牲一致性让系统更快从故障中恢复。2PC和3PC就是两条不同的设计路线。两阶段提交Two-Phase Commit选择的是“宁可阻塞也不能乱来”三阶段提交Three-Phase Commit则是想通过多增加一个阶段和超时自决机制解决阻塞问题。下面我先把两个协议完整拆开再进入故障场景对比。2. 两阶段提交2PC经典协议到底怎么跑2.1 第一阶段Prepare投票先别提交先表态2PC的第一个阶段叫准备阶段也叫投票阶段。协调者向事务涉及的所有参与者广播Prepare请求请求里带着全局事务ID和要执行的操作内容。参与者收到Prepare之后不是简单回答“我能接受”而是真的去执行本地事务的写操作只是先不提交。执行过程中要写undo log和redo log确保自己也能崩溃恢复同时相关数据行上的锁会被持有。一切就绪后参与者向协调者返回一个投票结果返回“OK/可以提交”说明本地操作已执行且日志已落盘我有能力提交返回“No/不能提交”说明本地执行失败或发现冲突我不支持这个事务继续。这里有个很容易被忽略的关键点参与者在Prepare阶段就已经把事务相关的锁占住了而且数据是不对其他人可见的。别人想读这一行会像撞上一堵墙一样被阻塞想写这一行更会直接锁等待。如果任何一个参与者投了No协调者不需要再等待其他人可以提前进入中止流程。但为了避免各种消息丢失的意外通常协调者还是会等到收集完所有回复或者超时后再统一决策。2.2 第二阶段Commit或Abort统一拍板当协调者收到所有参与者的投票进入提交阶段。投票全部OK广播Commit只要有一个No广播Abort。参与者收到Commit命令后把本地事务正式提交释放所有锁然后返回确认收到Abort命令则回滚本地事务并释放锁。整个事务到此完成。但这里有一个至关重要的细节协调者在广播Commit命令之前必须先把自己“决定提交”的日志写入并持久化。为什么要这么做因为如果协调者刚广播Commit命令就崩溃了它恢复之后需要根据日志知道“我已经决定过提交这个事务”从而向没收到命令的参与者补发Commit如果它没写日志就崩溃恢复后就不知道该事务到底该提交还是回滚整个事务状态就成了悬案。所以这个“决策日志”是整个2PC一致性的定音锤没有它协调者的记忆就像断电后白板的电脑根本没法恢复现场。2.3 2PC的两个大痛点同步阻塞与协调者单点我在生产环境遇到过一个特别典型的场景事务涉及6个分库前5个分库都完成了Prepare第6个分库因为磁盘慢盘迟迟不返回投票结果。协调者卡在等待阶段前5个分库的所有事务锁都干等在那里。结果就是不只是这个事务无法结束连其他访问同一批数据行的常规业务也被拖住数据库连接池很快被耗尽监控面板上一片飘红。这就是2PC的第一个痛点同步阻塞。更麻烦的是协调者本身也可能挂掉。举个具体例子协调者已经收到全部OK也写好了提交日志但在广播Commit的一瞬间进程崩溃了。此时所有参与者都没有收到Commit命令按照2PC规则它们既不能擅自提交也不能擅自回滚只能阻塞在原地持续持有锁和事务资源直到协调者恢复并重新联系上它们。要是协调者恢复不了参与者就会一直堵下去。生产上你不可能让一个事务等几小时所以通常还会额外做一套超时释放机制但那已经是脱离2PC协议的“外部补丁”了。这引出了2PC最被诟病的结构性问题它保证安全但不保证活性。它能保证整件事从头到尾不会出现“一个节点提交、另一个节点回滚”的不一致代价是当协调者故障或极端网络恶化时系统会卡死而不是继续向前走。数据库里占用的锁不释放业务吞吐就会被直接打成零。3. 三阶段提交3PC多出的PreCommit到底改变了什么3.1 三阶段流程拆解CanCommit、PreCommit、DoCommit3PC把2PC的两阶段扩展成了三阶段分别是CanCommit、PreCommit、DoCommit。理解它设计思路的关键在于看清楚每阶段“参与者做了多少实事”。第一阶段CanCommit也叫询问阶段。协调者只问参与者一句话“这个事务你有能力提交吗”参与者此时不执行任何事务写操作只根据自己当前的事务状态和锁冲突情况判断一下回答“能”或“不能”。如果有一个参与者说不能协调者当场决定Abort提前收工。第二阶段PreCommit也叫预提交阶段。协调者收到全部“能”之后广播PreCommit命令。参与者这时候才真正执行本地事务的写操作记录undo/redo日志进入“已准备好提交”的状态然后给协调者返回ACK。第三阶段DoCommit也叫最终提交阶段。协调者收到所有参与者的ACK后广播DoCommit命令。参与者收到后正式提交本地事务释放锁资源。如果中途出现异常或者协调者发了Abort参与者回滚。你可以明显看到3PC把2PC的“Prepare”拆成了“先问可不可以”和“再真正执行”。这个拆分让两个动作之间多了一个信息同步点为后面的超时自决埋下了伏笔。3.2 超时规则3PC减少阻塞的关键设计2PC最让人抓狂的是参与者一旦进入等待最终决定的状态就只能无限期傻等。3PC的设计目标就是给参与者设置明确超时后的默认动作让它们在被协调者抛弃时还能自己往前走。规则具体是这样参与者如果在CanCommit阶段等待协调者响应时超时按“Abort”处理认为事务终止参与者如果在PreCommit阶段等待DoCommit或Abort命令时超时按“Commit”处理因为既然大家已经完成了预提交全局决定基本就是提交方向即使协调者失联参与者也有理由自行完成提交。这套规则至少从理论上解决了2PC“协调者崩溃参与者全部卡死”的最坏情况。协调者挂了参与者不再像断电的路口一样堵死而是靠超时规则快速自决系统的可用性明显提升。3.3 好多人没看清的代价脑裂风险如果真实环境只是“协调者进程崩溃但节点之间网络都通”3PC这套超时自决确实好使。怕就怕网络分区也就是协调者和部分参与者之间彻底断开连接。我用一个3节点场景说明。事务涉及A、B、C三个参与者协调者给所有人广播了PreCommit但PreCommit消息只成功到达了A同时协调者所在的网络区域和B、C完全失联。A收到了PreCommit进入预提交状态之后迟迟等不到DoCommit按超时规则自行CommitB、C甚至根本没收到PreCommit或者停留在更早的等待状态超时后按规则Abort。最终结果同一个事务A提交B和C回滚。原子性直接被打破数据不一致产生了而且没有自动恢复的机制只能人工对账修复。这就是3PC的脑裂风险。说白了3PC是用“在特定故障下牺牲安全性”换来了“减少阻塞”。这个交易在理论模型里可能是划得来的但在真实系统里一旦出现脑裂你面对的就是数据错账、补单、人工核对这种极难收拾的场面。4. 2PC与3PC关键差异对比从机制到故障模型4.1 协议机制对比表把两套协议的差异摆成表格会直观很多对比维度2PC3PC阶段划分Prepare、Commit/Abort共2阶段CanCommit、PreCommit、DoCommit共3阶段参与者执行写操作时机第一阶段Prepare就执行第二阶段PreCommit才执行协调者正常时的消息开销相对较少多一次交互开销更大协调者故障后的参与者行为阻塞无限等待恢复按超时规则自决CanCommit超时AbortPreCommit超时Commit网络分区不会脑裂但可能长时阻塞可能脑裂出现部分提交安全性一致性保证异步网络下也能保证只在同步网络假设下保证活性可用性保证差协调者故障时卡壳好超时后能快速自决复杂度相对低状态机复杂实现难度更高实际部署情况XA事务、分布式数据库内核等广泛使用极少生产落地主要出现在论文和面试题中4.2 “提升活性、牺牲安全性”的准确含义分布式系统里有两个基础概念安全性和活性。安全性指的是“坏事永远不会发生”比如绝不允许出现部分提交活性指的是“好事最终一定会发生”比如事务最终能提交或回滚而不是无限期卡住。2PC的取舍非常清晰它把安全性放在第一位哪怕协调者跟所有参与者失联也不会有人擅自提交最多就是大家都阻塞住。阻塞就是活性的丧失但换来的是“不会出一个不一致的最终状态”。3PC则是往活性方向挪了一步当协调者长时间失联参与者通过超时规则自己拍板事务链能尽快恢复。但“自己拍板”一定依赖“自己对当时全局状态有准确判断”。一旦网络分区导致各参与者看到的全局状态不一致就会出现部分参与者提交、部分回滚的脑裂这正是安全性被牺牲的时刻。很多人在面试里讲不清楚“3PC为什么没有取代2PC”核心就在这儿2PC和3PC不是“谁更先进”而是分别选择了安全性优先和活性优先的路线。实际业务系统最怕的恰恰是脑裂所以大家宁可接受“最坏情况阻塞”的2PC也不愿意接受“最坏情况不一致”的3PC。4.3 同步网络假设3PC成立的前提条件细看3PC的超时逻辑你会发现它默认了一个前提超时是可靠的故障信号。也就是说当参与者等不到协调者消息时它可以认定协调者真的挂了或被隔离了而不是消息晚了几秒才到。这个前提成立的条件是消息延迟存在一个已知的上界节点处理速度也有一个已知的下界也就是理论里常说的同步网络模型。现实世界算同步网络吗不算。跨机房的公网延迟可以因为一次网络抖动从5毫秒涨到5秒虚拟机上的GC暂停能让一个节点处于“看起来死了”的状态长达几十秒磁盘慢盘、CPU争抢都会让消息传输和处理时间变得不可预测。这些都是异步网络的特征。一旦超时不可靠3PC的超时自决就会变成“拍脑袋自决”脑裂风险被进一步放大。这也是理论界很早就证明过的一件事在异步系统模型中没有一个确定性协议能同时保证分布式原子提交的安全性和活性。2PC牺牲活性换安全性3PC牺牲安全性换活性都是有代价的。5. 主流系统的真实选择为什么2PC屡屡被改造、3PC几乎没人部署5.1 2PC的工程化形态XA与数据库事务2PC虽然“理想很丰满现实很骨感”但它在工程界还是落地了不少最出名的就是XA规范。XA是X/Open组织制定的分布式事务接口标准定义了事务管理器TM和资源管理器RM之间的交互规范。MySQL的XA事务、PostgreSQL的两阶段提交以及很多消息中间件的分布式事务支持底层遵循的都是2PC思路。一次典型XA流程是这样的应用程序连接多个数据库通过TM发起XA事务依次在每个库执行操作并调用xa_end结束分支然后对所有分支执行xa_prepare全部成功后执行xa_commit。命令层面的xa_start、xa_end、xa_prepare、xa_commit、xa_rollback就是2PC协议在数据库端的映射。XA能落地是因为数据库自己把“Prepare状态”“日志持久化”“锁管理”都做了开发者只需要在业务代码里调用TM接口。但XA的代价也很直接全局锁持有时间长Prepare阶段所有资源都被占用高并发下数据库连接池和锁等待都容易爆。所以很多互联网公司只把XA用在账务、清结算这类强一致且并发可控的核心链路日常几千TPS以上的业务场景很少直接上XA。5.2 破局方向高可用协调者Raft而不是3PC那么2PC的协调者单点和阻塞问题业界到底是怎么解决的答案不是换成3PC而是把协调者本身做成高可用同时让参与者的恢复不再依赖单个协调者的“记忆”。最常见的组合是“2PC Raft/Paxos”。把协调者的决策日志通过一致性协议复制到多个副本节点协调者崩溃后通过选举机制产生新的协调者新协调者从多数派日志中恢复所有未决事务的状态然后继续广播之前没有送达的Commit或Abort命令。参与者只要能和这个高可用协调者集群通信就能最终知道该提交还是回滚不需要脑裂式的自行决策。这个思路之所以成为主流是因为它把2PC的安全性优势和Raft的可用性优势叠在了一起一致性交给提交协议保证协调者高可用交给共识协议保证。很多分布式数据库的原子提交、Google Spanner的跨分片事务本质上都有类似设计。和3PC相比这种方案没有牺牲一个核心性质即使在网络分区情况下只要多数派协调者还在事务就不会脑裂。5.3 面向业务的替代方案Saga、TCC和本地消息表再往上层走很多微服务场景根本不需要强一致的全局原子性业务上允许“最终一致”。这时候大家不用2PC而用Saga或TCC。Saga把一个全局事务拆成一系列本地事务每个本地事务执行成功后都登记一个补偿操作。如果某个步骤失败了就逆序执行之前所有步骤的补偿操作把数据回退到起点。它的特点是事务时长可控、不长期占用数据库锁适合下单、退款、订单状态流转这类跨服务长流程业务。TCC则把一个全局事务显式分成Try、Confirm、Cancel三个阶段资源预留和资源确认分开。Try阶段做业务检查和资源冻结Confirm阶段真正执行业务Cancel阶段释放冻结资源。它比Saga更精细适合需要预留资源、不希望中途长时间占锁的场景实现复杂度也更高。本地消息表则更轻量业务在自己数据库里同时写业务表和消息表这两个操作包在同一个本地事务中然后通过消息中间件把消息投递给下游下游消费时做幂等处理。这个方案不依赖全局协调者扩展性好缺点是业务侵入性强、对消息不丢失和幂等要求高。选型时我自己的判断是底层存储强一致且短事务用2PC或XA没问题跨服务长流程、能接受最终一致优先Saga/TCC3PC更多是一种理论参照物告诉你“为什么超时自决这条路走不通”而不是一个可以直接拿来部署的成熟方案。6. 我的选型建议、踩坑记录和面试回答框架6.1 选型决策先回答三个问题再动手被问得多了之后我总结出一套很简单的选型判断方法不看协议本身先回答三个问题业务能否接受短时间内数据不一致但最终一致如果可以选Saga或本地消息表。是否需要强一致的全局最终状态如果需要再看事务粒度和并发量。事务执行时间是否控制在秒级以内、并发是否可控如果是2PC/XA可用否则优先拆分事务或者用TCC做资源预留。我之前做过一个账务批处理系统200个分库同时开启事务用XA做全局原子提交。结果是某个分库磁盘IO抖动Prepare阶段一个库卡了十几秒其他分库全部锁等待超时回滚重试三次都失败。那次之后我彻底明白2PC不是不能用而是它要求事务短、网络稳、资源不争抢。一旦事务执行时间超过几秒就应该立刻换成按账户维度拆分、串行处理、Saga补偿的方案否则数据库连接池和锁等待会拖垮整个集群。6.2 实操中最容易忽略的三个细节第一协调者的决策日志必须落盘后再广播最终命令。我见过有人为了性能优化把决定先发给部分参与者再异步写日志。结果协调者崩溃日志丢了而参与者的状态已经五花八门恢复时完全不知道应该以谁为准。这是血泪教训2PC的日志写盘顺序是协议一致性的一部分不是一个可以随便调整的性能参数。第二全局事务ID和参与者状态机必须完整。每个参与者需要记录自己处于PREPARED、COMMITTED还是ABORTED状态协调者恢复后才能扫描所有分支、重新发送未完成的命令。没有全局事务ID索引你连“这事务涉及哪些库”都查不出来排障时完全是黑夜摸路。第三监控要能看见锁等待和Prepare耗时。我每次做分布式事务都会盯着几个指标Prepare成功率、从Prepare到Commit的耗时、锁等待超时次数、协调者日志大小。这些指标能最早告诉你事务是不是开始变慢、会不会把连接池打爆。不要等出故障了才想起来看日志事务协议的问题大多是慢热型的监控提早半天就能发现苗头。6.3 面试和交流中怎么讲才能讲到点子上如果要把2PC和3PC讲得让面试官或同事觉得你是真懂而不是背书我的习惯是分五步走一句话说清要解决的问题多个独立节点上的子事务如何实现全局原子提交。完整画出2PC的Prepare和Commit两个阶段点出“参与者在Prepare已执行写操作并持有锁”“协调者先写日志再广播”这两个关键细节。画出3PC的CanCommit、PreCommit、DoCommit三个阶段说明超时规则让参与者能自决。用一个网络分区案例说明3PC为什么会产生脑裂并点出它依赖同步网络假设。最后落到现实2PC通过XA在数据库领域广泛落地通过2PCRaft解决协调者高可用而3PC几乎难觅踪影。这一套讲下来对方基本能听出你不只是在背八股文。尤其是第4步能把“3PC为什么没取代2PC”从头到尾讲通透的人在团队里往往就是那个真正能拍板分布式事务方案的人。最后分享一点我个人的实操体会不要纠结于2PC和3PC之间“谁打败了谁”它们是同一枚硬币的两面一面写着安全性一面写着活性。做系统选型时先掂量清楚“你愿不愿意为了一致性付出可用性”再回头审视这套协议是否适合你的业务体量。把2PC和3PC吃透之后再去读Paxos Commit、去理解带高可用协调者的原子提交实现你会发现很多原本模糊的概念像齿轮一样咔哒咬合到了一起那时你才算真正把这个经典问题消化掉了。