3. 分布式事务基石:两阶段提交(2PC)与三阶段提交(3PC)的原理,为什么它们在实时场景下不够用?

聊到数据一致性,分布式事务是个绕不开的话题。很多同学一上来就问我:“能不能直接用两阶段提交(2PC)来保证实时数仓的一致性?”

我的回答通常是:理论上可以,但现实很骨感。我在早期做实时数仓项目时,也天真地想过用 2PC 来解决跨系统的一致性问题,结果被现实狠狠教育了一顿。

今天我们就来扒一扒 2PC 和 3PC 的底裤,看看它们到底是怎么工作的,又为什么在实时场景下“水土不服”。

3.1 两阶段提交(2PC)—— 一个“霸道总裁”式的协调者

2PC 的核心思想很简单:有一个“协调者”(Coordinator),它说了算。所有参与者(Participant)都得听它的。

整个过程分两个阶段:

  1. 准备阶段(Vote Phase):协调者问所有参与者:“我要提交事务了,你们准备好了吗?” 参与者各自执行事务,但不提交,然后回复“准备好了”(Yes)或“不行”(No)。
  2. 提交阶段(Commit Phase):如果所有人都说“准备好了”,协调者就广播“提交!”;如果有人说了“不行”,协调者就广播“回滚!”。

听起来很完美,对吧?但问题就出在这个“霸道总裁”身上。

2PC 的核心痛点:

  • 同步阻塞:在准备阶段,参与者会锁住资源(比如数据库的行锁),直到收到协调者的最终指令。如果协调者挂了,参与者就得一直等,资源一直被锁着。
  • 单点故障:协调者是整个系统的单点。它一旦宕机,整个事务就卡住了。
  • 数据不一致风险:如果在提交阶段,协调者广播了“提交”,但只有部分参与者收到了,那就会出现一部分提交了、一部分没提交的尴尬局面。

我记得有一次,我们用一个 2PC 方案去同步两个 MySQL 实例的数据。结果协调者所在的机器突然断电了,重启后,一个库提交了,另一个库还在“准备”状态。最后我们只能手动去查 binlog 来修复,那叫一个痛苦。

3.2 三阶段提交(3PC)—— 一个“民主协商”式的改进

为了解决 2PC 的阻塞问题,有人提出了 3PC。它把 2PC 的“准备阶段”拆成了两步:CanCommit 和 PreCommit,再加上一个 DoCommit。

阶段 动作 说明
CanCommit 协调者问:“能提交吗?” 参与者只检查自身状态(比如资源是否可用),不执行事务,不锁资源。
PreCommit 协调者说:“准备提交!” 参与者执行事务,写入 undo/redo 日志,但不提交。此时会锁资源。
DoCommit 协调者说:“正式提交!” 参与者正式提交事务,释放资源。

3PC 引入了超时机制。如果参与者在 PreCommit 阶段后没收到 DoCommit 指令,它会自动提交(而不是一直等)。这在一定程度上解决了 2PC 的阻塞问题。

但说实话,3PC 并没有从根本上解决数据不一致的问题。它只是把风险从“协调者挂了”转移到了“网络分区”上。如果网络出现分区,一部分参与者收到了 DoCommit,另一部分没收到,那数据还是不一致。

避坑指南:我曾经在一个跨机房的项目中尝试过 3PC。结果因为机房之间的网络抖动,导致部分节点在 PreCommit 后超时自动提交了,而协调者那边因为网络问题认为它们失败了,最终发起了回滚。结果就是:一部分数据提交了,一部分回滚了,整个系统乱成一锅粥。

3.3 为什么它们在实时场景下不够用?

好了,原理讲完了。现在我们来聊聊核心问题:为什么 2PC 和 3PC 在实时数仓里“不够用”?

说白了,实时数仓追求的是低延迟和高吞吐。而 2PC/3PC 的“同步阻塞”特性,恰恰是这两个目标的死对头。

  • 延迟太高:2PC 需要两轮网络通信,3PC 需要三轮。每一轮通信都伴随着锁等待。在实时场景下,一个事务可能涉及多个数据源(Kafka、HBase、Redis 等),每多一次网络交互,延迟就增加几十毫秒。对于要求秒级甚至毫秒级响应的实时系统来说,这简直是灾难。
  • 吞吐量上不去:因为要锁资源,并发度被严重限制。你想想看,如果 100 个事务同时进来,每个事务都要锁住一批数据,那系统很快就变成串行执行了。我见过一个项目,用了 2PC 后,TPS 直接从 5000 掉到了 200,业务方直接炸毛。
  • 资源开销大:每个参与者都需要维护事务日志(undo/redo),协调者需要维护事务状态。在实时数仓中,数据量是海量的,这种开销会迅速耗尽系统资源。
  • 对异构系统不友好:实时数仓通常由多种异构系统组成(Kafka、Flink、HBase、Elasticsearch 等)。要让这些系统都支持 2PC/3PC 协议,几乎是不可能的。很多系统(比如 Kafka)本身就不支持 XA 事务。

我的个人习惯:在实时数仓中,我几乎不会使用 2PC 或 3PC 来保证跨系统的一致性。我更倾向于使用“最终一致性”方案,比如“事务消息 + 本地消息表”或者“基于日志的 CDC(Change Data Capture)”。这些方案虽然不能保证强一致性,但能很好地平衡延迟、吞吐和一致性。

3.4 一张图看懂:2PC vs 3PC 在实时场景下的困境

下面这张图,我画出了 2PC 和 3PC 在实时数仓中可能遇到的典型问题。你可以看到,从协调者到参与者的每一步,都可能成为瓶颈。

2PC/3PC 在实时场景下的困境 2PC/3PC 在实时场景下的困境 协调者 (Coordinator) 参与者 A (Kafka) 参与者 B (HBase) 参与者 C (Redis) 问题1: 锁等待 问题2: 网络延迟 问题3: 单点故障 实时场景下的核心矛盾 • 2PC/3PC 要求所有参与者同步响应,但实时系统追求低延迟。 • 锁资源导致并发度下降,无法支撑高吞吐。 • 异构系统(Kafka、HBase)难以统一支持 XA 协议。

3.5 小结:实时数仓需要什么样的“一致性”?

看到这里,你应该明白了:2PC 和 3PC 是分布式事务的“基石”,但它们是为“强一致性”场景设计的。在实时数仓中,我们往往需要的是“最终一致性”或“可接受的弱一致性”。

我个人认为,实时数仓的数据一致性保障,不应该依赖一个“万能”的分布式事务协议。而是应该根据不同的业务场景,组合使用不同的策略:

  • 对于核心链路(比如订单支付),可以使用“事务消息 + 本地消息表”来保证最终一致性。
  • 对于非核心链路(比如用户行为日志),可以使用“幂等写入 + 去重”来容忍短暂的不一致。
  • 对于跨系统同步,可以使用“基于 CDC 的日志解析”来实现准实时同步。

嗯,这些内容我们会在后面的章节中详细展开。今天先把 2PC 和 3PC 的底牌摸清楚,后面再聊具体方案时,你就能明白为什么我们要“绕开”它们了。