2. 强一致性方案:2PC(两阶段提交)原理、优缺点与协调者单点问题
聊到分布式事务,很多人第一个想到的就是两阶段提交(2PC)。
说实话,2PC 是个老古董了。早在 80 年代的数据库论文里就有它的身影。但直到今天,很多金融、支付系统里依然在用。为什么?因为它够简单,够直接,能保证强一致性。
我当年刚接触分布式系统时,第一个学的分布式事务协议就是 2PC。当时觉得这玩意儿太直观了——不就是先问一圈,再统一执行吗?后来踩了坑才发现,事情没那么简单。
2.1 两阶段提交的核心思想
2PC 把一次分布式事务拆成两个阶段:准备阶段和提交阶段。
参与的角色有两个:
- 协调者(Coordinator):通常是事务管理器,负责调度所有参与者。
- 参与者(Participant):一般是各个数据库或资源管理器,负责执行本地事务。
说白了,协调者就是那个发号施令的人。参与者就是干活的人。
2.2 第一阶段:准备阶段(Voting Phase)
这个阶段,协调者会问所有参与者:「我要提交事务了,你们准备好了吗?」
具体流程是这样的:
- 协调者向所有参与者发送
prepare请求。 - 每个参与者收到请求后,执行本地事务,但不提交。
- 参与者把执行结果(Undo/Redo 日志)写入本地磁盘。
- 如果执行成功,返回
Yes;如果失败,返回No。
关键点:参与者一旦回复 Yes,就意味着它承诺——哪怕之后宕机重启,也要保证能提交这个事务。因为日志已经落盘了。
嗯,这里要注意。我在项目中遇到过一种情况:参与者回复了 Yes,但协调者迟迟没给下一步指令。这时候参与者只能一直等,资源也被锁着。这就是 2PC 的阻塞问题,后面会细说。
2.3 第二阶段:提交阶段(Commit Phase)
第二阶段就简单了。协调者根据第一阶段的投票结果做决策:
- 全部 Yes:发送
commit指令,所有参与者提交事务。 - 有 No 或超时:发送
abort指令,所有参与者回滚事务。
你想想看,这个逻辑其实很符合直觉。就像团队开会,先问一圈「方案行不行」,大家都说行,那就执行。有人说不行,那就推翻重来。
但问题在于——如果协调者在第二阶段挂了,怎么办?
2.4 2PC 的优缺点分析
先说说优点,毕竟它能在业界活这么多年,肯定有它的道理。
优点
- 强一致性:要么全部提交,要么全部回滚。数据不会出现部分更新。
- 实现简单:协议本身很直观,容易理解和编码。
- 兼容性好:很多数据库(MySQL、Oracle、PostgreSQL)原生支持 XA 协议,可以直接用 2PC。
我记得有一次做银行核心系统改造,老系统用的是本地事务,迁移到分布式架构后,业务方最担心的就是数据不一致。最后我们选了 2PC,虽然性能有损耗,但至少账不会错。
缺点
缺点嘛,说实话,挺多的。我一个个说。
- 同步阻塞:参与者准备阶段会锁住资源。如果协调者挂了,参与者只能一直等。系统吞吐量直接打折扣。
- 单点故障:协调者是单点。它一挂,整个事务就卡住了。
- 数据不一致风险:第二阶段,协调者发送
commit后挂了。部分参与者收到了指令并提交,部分没收到。数据就不一致了。 - 性能问题:两轮网络交互,加上磁盘日志写入,延迟很高。不适合高并发场景。
避坑指南:我曾经在一个秒杀系统里尝试用 2PC 做库存扣减。结果压测时,数据库连接池直接被打满。所有线程都在等锁。后来换成了 TCC 模式才解决问题。所以,高并发场景下慎用 2PC。
2.5 协调者单点问题深度剖析
协调者单点问题,是 2PC 最致命的缺陷之一。
为什么会这样?因为整个事务的进度都依赖协调者。它一旦宕机,所有参与者都会陷入「悬而未决」的状态。
具体来说,分两种情况:
| 故障时机 | 影响 | 恢复方式 |
|---|---|---|
| 第一阶段宕机 | 参与者还没收到 prepare,事务直接超时回滚 | 参与者超时检测,自动回滚 |
| 第二阶段宕机 | 部分参与者已提交,部分未提交,数据不一致 | 需要人工介入或协调者恢复后查询日志 |
你看,第二阶段宕机是最麻烦的。参与者不知道是该提交还是该回滚。只能一直等协调者恢复。
我个人的习惯是,在生产环境中给协调者做高可用。比如用 ZooKeeper 或 etcd 做选主,协调者挂了自动切换。但即便如此,切换期间的事务还是可能卡住。
我的建议:如果业务对一致性要求极高,且能接受一定的性能损耗,可以考虑 2PC + 协调者主备切换。但一定要做好超时补偿机制。比如,参与者可以设置一个超时时间,超时后主动查询协调者状态。
2.6 2PC 的典型应用场景
说了这么多缺点,那 2PC 到底适合什么场景?
- 金融转账:A 扣钱,B 加钱。必须同时成功或失败。
- 库存扣减:多个仓库同时扣库存,不能出现超卖。
- 订单与支付:订单状态和支付状态必须一致。
说白了,就是那些「一分钱都不能错」的场景。性能可以慢,但数据必须准。
我记得之前做一个跨境支付系统,对接了多个国家的银行。每个银行都有自己的数据库,事务必须跨库。最后我们选了 2PC + XA 协议。虽然延迟高了点,但上线两年没出过数据不一致的问题。
2.7 代码示例:一个简化的 2PC 协调者
下面我用伪代码展示一下协调者的核心逻辑。方便你理解整个流程。
class Coordinator {
List<Participant> participants;
boolean executeTransaction() {
// 第一阶段:准备
for (Participant p : participants) {
boolean ok = p.prepare();
if (!ok) {
// 有参与者准备失败,发起回滚
rollback();
return false;
}
}
// 第二阶段:提交
for (Participant p : participants) {
p.commit();
}
return true;
}
void rollback() {
for (Participant p : participants) {
p.abort();
}
}
}
你看,代码本身很简单。但实际生产环境里,要考虑网络超时、宕机恢复、日志持久化等问题。复杂度一下子就上去了。
2.8 小结
2PC 是分布式事务的入门级方案。它简单、直观、能保证强一致性。但缺点也很明显:阻塞、单点、性能差。
我个人觉得,2PC 适合那些对一致性要求极高、并发量不大的场景。如果你的系统需要高吞吐,或者服务经常宕机,那还是看看 TCC 或 Saga 吧。
下一章,我会讲 3PC(三阶段提交),它解决了 2PC 的阻塞问题,但引入了新的复杂度。到时候咱们再细聊。