当一笔订单需要同时扣减库存、更新账户余额并写入物流记录时,这些数据可能分散在不同数据库甚至不同机器上。单机事务的ACID保障在这里瞬间失效,任何一个环节失败而其他环节已提交,数据就会出现不一致。分布式事务正是为了解决这个问题,而两阶段提交(Two-Phase Commit,简称2PC)则是其中最经典、最具代表性的协议。理解它的原理与局限,是做架构选型绕不开的一课。

两阶段提交的完整执行流程
2PC的核心角色有两个:协调者(Coordinator)和参与者(Participant)。整个协议分为准备阶段和提交阶段。在准备阶段,协调者向所有参与者发送prepare请求,参与者执行事务操作但并不真正提交,而是把 undo 和 redo 日志写入磁盘,然后回复 yes 或 no。如果所有参与者都回复 yes,协调者进入提交阶段,发送 commit 指令;只要有任何一个参与者回复 no,或者超时未响应,协调者就发送 rollback 指令,所有参与者回滚。
用一个简化的伪代码来描述这个过程:
// 协调者逻辑(简化版)
boolean allYes = true;
for (Participant p : participants) {
if (!p.prepare(txId)) { // 第一阶段:询问是否可以提交
allYes = false;
break;
}
}
if (allYes) {
for (Participant p : participants) {
p.commit(txId); // 第二阶段:全部同意则提交
}
} else {
for (Participant p : participants) {
p.rollback(txId); // 任一拒绝则全部回滚
}
}这个过程看似简单,但每个细节都决定了协议的正确性。参与者在回复 yes 之前必须保证事务日志已经持久化到磁盘,否则协调者后续发来 commit 指令时,参与者可能已经宕机重启,丢失了事务上下文,无法完成提交。这也是为什么数据库层面的 XA 事务在 prepare 时通常伴随着一次强制刷盘,代价不低。
主流数据库和消息中间件对2PC的支持主要体现为 XA 协议,例如 MySQL 的 XA START、XA END、XA PREPARE、XA COMMIT 这一套语句。Java 生态中的 JTA 接口则封装了这些底层操作,让应用可以通过注解或编程方式发起分布式事务。
2PC的致命短板:阻塞、单点与数据不一致
2PC最被诟病的问题是同步阻塞。在第一阶段投票完成到第二阶段指令下达之间,所有参与者都必须持有锁并等待,期间相关资源无法被其他事务访问。并发越高、参与者越多,锁持有时间越长,吞吐量下降越明显。对于互联网高并发场景,这种代价往往难以接受。
第二个问题是协调者单点故障。如果协调者在发送 commit 指令前一刻宕机,参与者会处于既不能提交也不能回滚的悬而未决状态,只能一直持有锁等待协调者恢复。虽然可以通过为协调者引入日志持久化和主备切换来缓解,但这又增加了系统复杂度。
第三,2PC在网络分区下存在数据不一致的窗口。考虑这样一个场景:协调者决定提交,向部分参与者发送了 commit,此时网络中断,另一部分参与者没有收到指令。协调者随后宕机,新选出的协调者通过日志得知事务应提交,但此时部分参与者可能因为超时自行回滚了(取决于具体实现),就出现了有的节点提交、有的节点回滚的脑裂状态。严格来说,标准2PC无法完全避免这种不一致,工程上通常依靠超时策略和协调者日志恢复来收敛。
总结来看,2PC提供的是一种偏强一致性的保障,但代价是可用性和性能。根据 CAP 定理的取舍逻辑,它天然不适合对吞吐和可用性要求极高的系统。
替代方案对比:三阶段提交、TCC、Saga与消息最终一致性
三阶段提交(3PC)在2PC基础上增加了 CanCommit 询问阶段,并引入了超时自动提交机制,缓解了阻塞问题和协调者单点带来的悬而未决状态。但3PC仍然无法完全规避网络分区下的不一致,且多一次交互进一步增加了延迟,实际生产中使用并不广泛,更多是作为理论演进的一环。
TCC(Try-Confirm-Cancel)模式把事务控制权上移到业务层。Try 阶段预留资源(如冻结账户金额),Confirm 阶段确认扣减,Cancel 阶段释放预留。它的优势是不依赖数据库锁,性能好,且每个阶段都是明确的业务动作,对异常的处理更精细。代价是业务侵入性强,每个参与方都要实现三个接口,并且要处理好幂等、空回滚和悬挂这三个经典问题。适合资金类、对一致性要求高且业务方有能力改造的场景。
Saga模式把长事务拆分为一串本地事务,每个本地事务都有对应的补偿动作。任一步骤失败,就逆向执行之前步骤的补偿。它没有锁定阶段,性能好,适合长流程、跨多系统的业务编排。但 Saga 只提供最终一致性,中间状态对用户可见,补偿逻辑的编写也考验业务理解。
基于消息的最终一致性方案则是电商领域最常用的做法:本地事务与消息发送绑定(通过本地消息表或事务消息如 RocketMQ 的事务消息),下游服务消费消息完成自己的本地事务,失败则重试。整个链路异步化,吞吐高,但同样只保证最终一致。
| 方案 | 一致性 | 性能 | 业务侵入 | 适用场景 |
|---|---|---|---|---|
| 2PC/XA | 强一致 | 低 | 低 | 传统企业应用、低并发跨库事务 |
| 3PC | 偏强一致 | 较低 | 低 | 理论研究为主,生产少见 |
| TCC | 最终一致(准实时) | 较高 | 高 | 金融、账户类核心链路 |
| Saga | 最终一致 | 高 | 中 | 长流程业务编排 |
| 事务消息 | 最终一致 | 高 | 中 | 异步解耦的互联网业务 |
如何为你的系统做权衡
选型的第一原则是:能不引入分布式事务就不引入。很多不一致问题可以通过合理的领域划分来消解,把强关联的数据放在同一个数据库、同一个服务里,用单机事务解决。跨服务的数据同步尽量走异步消息,容忍短暂不一致但通过对账和补偿机制兜底。
如果确实需要跨库强一致且并发不高,例如传统 ERP、财务系统,XA/2PC 依然是简单可靠的选择,不要为了技术先进性强行引入复杂方案。如果是对性能敏感的资金链路,TCC 配合幂等设计和防悬挂处理是更主流的方案,蚂蚁、携程等公司的实践都证明了其可行性。如果是订单、履约这类长流程编排,Saga 或状态机加补偿的模式更贴合业务形态。
最后,无论选择哪种方案,都要准备好三件配套工具:全局唯一的事务ID用于链路追踪、对账任务用于发现漏网的不一致、以及人工干预后台用于极端故障下的数据修复。分布式事务没有银弹,工程上的稳妥做法永远是:用最简单的方案满足一致性要求,再用监控和补偿体系覆盖方案覆盖不到的角落。