1. 从一次线上数据不一致说起为什么我们需要分布式事务协议去年我们团队负责的一个核心微服务系统上线了一个新功能涉及订单状态更新和积分发放。逻辑很简单用户支付成功后一个服务更新订单为“已完成”另一个服务给用户增加积分。开发环境、测试环境跑得都挺好结果一上线噩梦开始了。监控告警显示时不时有用户投诉“钱扣了但积分没到账”或者反过来“积分多了但订单还是待支付”。一查日志两个服务的数据库不在一个实例上网络偶尔抖动一下就可能一个成功一个失败。这就是典型的分布式事务问题如何保证跨多个独立资源数据库、服务的操作要么全部成功要么全部失败像一个整体一样为了解决这个问题我们不得不回过头来重新审视那些经典的分布式一致性协议。其中“二段式提交”和“三段式提交”是绕不开的基石。别看名字听起来像教科书里的老古董在当今的分布式数据库中间件、微服务事务解决方案里它们的灵魂无处不在。今天我就结合那次踩坑和后续的架构选型来彻底拆解一下这两个协议。我们不光要明白它们每一步在干什么更要理解它们设计背后的权衡、各自的致命软肋以及在实际工程中我们到底该怎么选、怎么用还有怎么绕着它们的坑走。2. 二段式提交简单粗暴的“全票通过制”二段式提交也叫2PC它的核心思想非常直观就像一个小组做决策有一个协调者Coordinator也叫事务管理器和若干个参与者Participants也叫资源管理器。它的目标就一个确保所有参与者对“提交事务”这个事达成完全一致。2.1 第一阶段投票表决这个阶段协调者不再是“命令者”而是变成了“询问者”。它的工作流程是这样的协调者发送准备请求协调者向所有参与者发送一个Prepare消息消息里包含了本次事务的所有操作内容。注意此时协调者只是问“兄弟我有个事要办具体事告诉你了你这边条件都OK不能办不” 它并不要求参与者立刻执行。参与者执行本地预提交每个参与者收到Prepare后会做几件至关重要的事将事务所需的所有数据修改写入本地磁盘的日志通常是Redo Log和Undo Log。这是为了确保即使后续进程崩溃也有能力继续完成或回滚。锁定事务涉及的相关数据行或资源防止其他并发事务干扰。完成所有本地能做的检查比如账户余额是否充足、唯一键是否冲突等。但是绝不进行最终的提交也就是说数据库里用户还看不到数据变化。参与者返回投票结果根据本地预提交的情况参与者向协调者回复消息。如果一切就绪回复Yes或Agree。如果任何一步出错如检查失败、磁盘写满、死锁回复No或Abort。注意一旦参与者回复了Yes它就进入了一种“承诺状态”。这意味着它已经向协调者做出了一个保证“只要最后你让我提交我一定能提交成功如果你让我回滚我也一定能用刚才记的日志回滚干净。” 这是一个不可撤销的承诺。2.2 第二阶段执行决议协调者收集所有参与者的投票。这里就是决策点情况一全票通过。如果协调者收到了所有参与者的Yes回复那么它就会做出提交的决定。协调者自己首先将“提交”这个决定持久化到日志防止自己中途挂掉。然后向所有参与者发送Commit消息。参与者收到Commit后正式提交本地事务释放锁定的资源并向协调者发送Ack确认消息。协调者收到所有Ack后整个事务完成。情况二一票否决。如果协调者收到了任何一个参与者的No回复或者等待回复超时那么它就会做出回滚的决定。协调者将“回滚”决定持久化到日志。向所有参与者发送Rollback消息。参与者收到Rollback后利用第一阶段保存的Undo日志进行回滚操作释放资源并回复Ack。协调者收到所有Ack后事务终止。2.3 2PC的“阿喀琉斯之踵”同步阻塞与单点故障2PC的逻辑清晰易懂但它有两个在分布式环境下非常要命的问题这也是我们当初没有直接采用原生2PC的原因。1. 同步阻塞问题在整个事务过程中资源是长期被锁定的。从参与者收到Prepare消息并锁定数据开始到最终收到Commit或Rollback消息为止这些数据一直处于不可用状态。如果协调者等待某个慢速参与者回复时卡住了或者网络延迟那么其他更早准备好的参与者也只能干等着它们锁住的数据无法被其他事务访问。这在高并发场景下会急剧降低系统吞吐量甚至引发死锁链。2. 单点故障与数据不一致风险这是2PC最被诟病的问题协调者成了整个系统的“命门”。协调者在发送Commit前崩溃此时部分参与者可能已经回复了Yes并进入了“承诺状态”它们在等待协调者的最终指令。由于协调者挂了没留下最终决定这些参与者将永久阻塞锁住的资源无法释放。它们只能等待协调者恢复或者由管理员人工介入查看日志来决定是提交还是回滚。这段时间系统部分功能不可用。协调者在发送Commit后崩溃这是更麻烦的情况。假设有两个参与者A和B。协调者发送Commit后A收到了并成功提交然后协调者崩溃了。B可能因为网络问题没收到Commit。当协调者恢复后它从日志里知道事务应该提交于是重新发送Commit给B。但如果B在等待期间超时它可能自行决定回滚因为它只收到过Prepare没收到最终指令。这就导致了数据不一致A提交了B回滚了。第二种情况引出了“三阶段提交”要解决的核心问题。3. 三段式提交引入“预提交”的缓冲地带三段式提交即3PC是针对2PC的阻塞和单点故障问题的一种改进方案。它在2PC的“投票”和“执行”之间硬生生插入了一个新的阶段叫做“预提交”。这个阶段的核心目的是让所有参与者在真正提交之前再次确认大家都准备好了从而降低阻塞时间和不一致的风险。3.1 第一阶段CanCommit询问阶段这个阶段比2PC的Prepare更“轻量”。协调者向所有参与者发送CanCommit请求。参与者进行可行性检查比如检查网络是否通畅、自身状态是否正常、资源是否可用。但不执行任何真正的数据操作也不写日志、不锁资源。参与者根据检查结果回复Yes或No。这个阶段就像战前动员“大家状态都好吗能打吗” 如果这时候就有人不行事务直接结束成本很低。3.2 第二阶段PreCommit预提交阶段只有所有参与者都回复了Yes协调者才会进入本阶段。协调者发送PreCommit请求。参与者收到后开始执行2PC中Prepare阶段的操作写Redo/Undo日志锁定资源执行本地检查。完成后回复Ack。这里有个关键设计如果参与者在这个阶段收不到协调者的消息超时它会默认执行事务回滚。因为这说明协调者可能在第一阶段就收到了No或者自己挂了所以事务应该中止。这避免了2PC中因协调者第一阶段崩溃而导致的永久阻塞。3.3 第三阶段DoCommit执行提交阶段协调者收到所有参与者的PreCommitAck后进入最终阶段。协调者自己先持久化“提交”决定然后发送DoCommit请求。参与者收到DoCommit后正式提交事务释放资源回复Ack。协调者收到所有Ack事务完成。另一个关键设计如果参与者在第三阶段等待DoCommit超时它会默认执行事务提交。为什么因为能走到第三阶段说明所有参与者在第二阶段都回复了Ack即大家都已经完成了预提交写了日志锁了资源都做好了提交的准备。此时协调者失联大家有理由相信协调者原本是要发DoCommit的只是消息丢了。为了不让系统阻塞大家“民主表决”一起提交。这解决了2PC中协调者在发送Commit后崩溃可能导致的数据不一致问题。3.4 3PC的进步与代价3PC通过增加一个阶段带来了两个主要好处减少了永久阻塞的可能性在PreCommit阶段超时参与者可以安全回滚。在DoCommit阶段超时参与者可以安全提交。系统在协调者故障后有一定自我恢复能力。降低了不一致窗口期由于PreCommit阶段让大家再次确认最终提交阶段出问题的概率相对降低。但是3PC的代价也很明显性能开销更大多了一次网络往返和消息广播延迟必然增加。并未彻底解决一致性问题在DoCommit阶段如果因为网络分区一部分参与者收到了DoCommit并提交另一部分参与者超时后也自行提交这看起来没问题。但如果网络分区恢复后协调者发现之前有参与者PreCommit失败消息延迟到达它可能会尝试发起回滚但此时部分节点已提交就会导致不一致。3PC只是降低了概率并未根除。实现更复杂超时后的默认行为第二阶段超时回滚第三阶段超时提交需要精确的状态机来维护实现难度比2PC高。4. 工程实践我们如何与这些协议共舞理解了原理和缺陷我们在实际项目中就不会直接去裸用2PC或3PC而是使用基于它们思想、但做了大量优化的工业级解决方案。4.1 分布式数据库与中间件中的2PC变种像MySQL的XA事务、Java的JTA规范其底层实现就是2PC。但在使用它们时我们非常谨慎超时与锁优化我们会设置较短的超时时间避免长时间阻塞。对于锁尽量使用更细粒度的行锁并让事务尽可能短小。协调者高可用对于协调者单点问题通常通过将协调者日志同步到备用节点、或者使用集群选举机制来保证高可用。例如一些分布式事务框架会有一个Leader作为协调者Follower同步日志Leader挂掉后能快速切换。最终一致性兜底对于一致性要求不是瞬间强一致的业务如我们的积分发放我们更倾向于采用“最终一致性”方案配合消息队列和补偿机制如TCC、Saga完全避免使用同步阻塞的2PC。那次线上问题我们最终就是用“本地消息表异步任务补偿”的方案解决的。4.2 3PC思想在共识算法中的体现3PC中“预提交”和“超时默认行为”的思想在更现代的共识算法如Paxos、Raft中得到了更精妙的应用。Raft算法中的“日志复制”阶段就类似于一个强化版的预提交需要大多数节点确认后才认为“准备成功”。Leader失效后Follower的超时选举机制也能保证系统快速恢复避免了无限期阻塞。4.3 选型心法CAP定理下的权衡选择方案时心里一定要装着CAP定理一致性、可用性、分区容错性三选二。2PC更偏向CP一致性与分区容错性。它牺牲了部分可用性阻塞期间服务受影响来保证强一致性。适用于对一致性要求极高、可以容忍短暂不可用的金融核心交易等场景。3PC试图在CP的基础上稍微提升一点A可用性通过引入超时机制减少阻塞时间但代价是更复杂的实现和依然存在的不一致风险。它像一个改良的CP方案。最终一致性方案如Saga、TCC更偏向AP可用性与分区容错性。它优先保证服务可用允许数据在短时间内不一致但通过异步补偿最终达到一致。适用于我们遇到的积分、库存、日志记录等大多数互联网业务场景。我的个人体会是在微服务架构下强一致性分布式事务2PC/3PC因其性能瓶颈和复杂性其适用面正在变窄。更多的场景是通过业务设计比如将需要强一致的操作收敛到同一个服务内数据库共享或者采用最终一致性对账补偿来化解。理解2PC和3PC更重要的是理解它们揭示的分布式系统核心矛盾——协调与代价、一致与可用这能帮助我们在设计系统时做出更清醒的架构决策。当你下次选择事务方案时不妨先问自己这个业务场景真的需要那一瞬间的强一致吗为了这一瞬间我们愿意付出阻塞和复杂性的代价吗答案往往就在问题本身之中。