4. 2PC实战与缺陷:同步阻塞、单点故障与数据不一致

2PC(两阶段提交)是分布式事务里最经典的协议之一。说实话,我最早接触它是在做库存管理系统的时候,当时觉得这玩意儿逻辑挺清晰的——先问大家能不能提交,再统一让提交。但真正落地之后,才发现坑比想象中多得多。

这一节,咱们就聊聊2PC在实际项目中会遇到哪些问题。我会结合自己踩过的坑,把同步阻塞、单点故障、数据不一致这几个核心缺陷掰开揉碎了讲。

4.1 2PC的核心流程回顾

先简单回顾一下2PC的两个阶段。嗯,这里我用一个库存扣减的场景来说明。

场景:用户下单扣减库存,涉及库存服务(A)和订单服务(B)。

// 第一阶段:准备阶段(投票阶段)
协调者 -> 参与者A: 准备提交(prepare)
协调者 -> 参与者B: 准备提交(prepare)
参与者A -> 协调者: OK(资源锁定成功)
参与者B -> 协调者: OK(资源锁定成功)

// 第二阶段:提交阶段(执行阶段)
协调者 -> 参与者A: 提交(commit)
协调者 -> 参与者B: 提交(commit)
参与者A -> 协调者: ACK
参与者B -> 协调者: ACK

看起来很简单对吧?但问题就出在这个「准备」和「提交」之间。

4.2 同步阻塞问题:资源被锁死了

2PC最大的痛点,就是同步阻塞。说白了,第一阶段参与者把资源锁住之后,必须等协调者发号施令才能释放。这个等待过程,其他事务只能干瞪眼。

我在项目中遇到过这样一个情况:库存服务A锁住了一批商品的库存,但协调者因为网络延迟,迟迟没发第二阶段指令。结果呢?其他想扣减这批库存的请求全部被阻塞,用户端直接超时。

注意:阻塞时间取决于协调者的响应速度。如果协调者挂了,资源可能被锁到超时为止。在高并发场景下,这几乎是灾难性的。

为什么会这样?因为2PC的设计里,参与者在第一阶段必须把资源从「可用」状态变成「预占」状态。这个预占状态,说白了就是「我可能要用,也可能不用,但别人不能用」。你想想看,这跟单机数据库的行锁有什么区别?只不过锁的范围从单机变成了分布式。

我个人习惯的做法是:尽量缩短第一阶段到第二阶段的时间窗口。比如把协调者和参与者部署在同一个机房,减少网络延迟。但说实话,这只是治标不治本。

4.3 单点故障风险:协调者挂了怎么办?

2PC里协调者是核心。它一旦挂了,整个事务就卡住了。我来说说两种典型情况。

4.3.1 协调者在第一阶段挂了

如果协调者在发送prepare请求之后、收到所有响应之前宕机,参与者会一直等待。直到超时,参与者才会主动释放资源。但这个超时时间怎么设?设短了,正常慢请求会被误杀;设长了,阻塞时间又太长。

4.3.2 协调者在第二阶段挂了

这个更麻烦。协调者已经发出了commit指令,但只发给了部分参与者。剩下的参与者收不到指令,就会一直处于「预占」状态。数据不一致就这么产生了。

避坑指南:我曾经在线上遇到过协调者进程OOM的情况。当时库存服务A收到了commit,但订单服务B没收到。结果库存扣了,订单没生成。最后只能靠人工脚本去对账修复。嗯,从那以后我养成了一个习惯——给协调者加一个持久化日志,记录它每一步的状态。这样即使挂了,重启后也能恢复。

4.4 数据不一致场景分析

数据不一致是2PC最致命的问题。理论上2PC能保证强一致性,但实际中因为网络分区、节点宕机等原因,不一致的情况时有发生。

我总结了几种典型场景:

场景 触发条件 结果
协调者发送commit时网络分区 部分参与者收到commit,部分没收到 已提交的节点数据变更,未提交的节点回滚
参与者收到commit后宕机 宕机前未持久化 重启后数据丢失,与其他节点不一致
协调者误判 参与者返回OK但实际资源不足 第二阶段提交时失败,但其他节点已提交

你看,这些场景本质上都是因为网络不可靠节点状态不可知。2PC假设网络是可靠的,但现实不是。

4.5 知识体系与核心逻辑

下面这张图,我把2PC的缺陷和它们之间的关系画出来了。你看一眼就能明白问题出在哪。

2PC三大缺陷与关联 2PC 两阶段提交 同步阻塞 资源锁定,等待协调者 单点故障 协调者宕机导致卡死 数据不一致 部分提交,部分回滚 阻塞加剧故障 故障导致不一致 后果:高并发下性能急剧下降,数据一致性难以保证 三个缺陷相互影响,实际项目中往往同时出现

从这张图你能看出来,三个缺陷其实是互相影响的。同步阻塞让系统变慢,单点故障让系统变脆弱,数据不一致让系统不可信。任何一个没处理好,整个分布式事务就崩了。

4.6 实战中的应对策略

说了这么多问题,那2PC是不是就不能用了?也不是。我在实际项目中总结了几条经验:

  • 超时机制必须加:给每个参与者设置合理的超时时间。我一般设成业务峰值的2倍,比如正常响应是100ms,超时就设200ms。
  • 协调者做高可用:用主备模式或者选举机制。协调者挂了,备机立刻顶上。但要注意状态同步,不然备机不知道当前事务到哪一步了。
  • 日志先行:协调者和参与者都写WAL(预写日志)。这样即使宕机,重启后也能根据日志恢复状态。
  • 补偿机制兜底:2PC不是万能的。我习惯在业务层加一个对账脚本,定期检查数据一致性。发现不一致就自动补偿。

个人经验:我曾经在一个库存系统中,把2PC和本地消息表结合使用。2PC负责核心的强一致性操作,本地消息表负责最终一致性。这样既保证了关键数据的准确,又避免了全链路阻塞。说白了,没有银弹,只有合适的组合。

好了,关于2PC的实战缺陷就聊到这。下一节咱们会讲3PC,看看它怎么尝试解决这些问题。但提前剧透一下——3PC也不是完美的。


公众号:蓝海资料掘金营,微信deep3321