Skip to content

分布式事务 ​

下单要同时扣库存、扣优惠券、生成订单,三个动作落在三个服务各自的数据库里。单库事务的 BEGIN 和 COMMIT 管不到别的库,任何一个动作失败,已经成功的动作没法自动回滚。分布式事务要解决的就是这件事,一次跨多个库的操作,怎么保证要么全成功要么全失败。

1. 单库事务为什么失效 ​

单库事务靠的是数据库的原子性。扣库存、扣优惠券、插订单在同一个库、同一个事务里,数据库保证这三条要么一起生效要么一起回滚。这个保证成立的前提,是它们都在一个数据库实例的一个事务里。

拆成微服务后,前提没了。库存服务、优惠券服务、订单服务各有一个库,三个库各自提交各自的事务,没有一个全局事务能同时管住它们。

下单流程变成三段独立提交。库存扣了,优惠券扣了,订单插入时失败。前两个库已经提交,回不去了。用户没下单成功,库存和优惠券却少了。这就是核心矛盾,本地事务管不了跨库的原子性。

2. 两条路线的分水岭 ​

所有分布式事务方案都逃不开一个取舍,一致性和性能只能二选一。强一致要求每一步都等所有参与者确认,代价是阻塞和性能损耗。最终一致允许中间态存在,靠后续补偿把状态追平,代价是短时间内数据对不上。

取舍的依据只有一个问题,业务能不能容忍中间态。下单扣库存这类操作,容忍不了「库存扣了但订单没生成」的中间态,才需要强一致。而电商里大多数场景其实能容忍,订单和库存短暂不一致,靠后续对账补平即可。

想清楚这一点,就不会一上来就上强一致。强一致方案复杂度高、性能差,大多数业务根本用不到。

3. 强一致路线,2PC 及其代价 ​

2PC 引入一个协调者,把提交拆成两个阶段。第一阶段问所有参与者「能不能提交」,第二阶段根据回答决定「都提交」还是「都回滚」。

txt
协调者                参与者A              参与者B
  │  ① 询问能否提交      │                    │
  ├──────────▶         │                    │
  │                     ├── ② 都回复 Yes ──▶ │
  │  ③ 通知提交         │                    │
  ├──────────▶         │                    │

2PC 保证了一致性,但代价有三条。

第一,阻塞。参与者回复 Yes 后,就处于等待协调者指令的状态,锁着资源不放。协调者一旦宕机,所有参与者卡在中间状态,事务悬挂。

第二,单点。协调者是整个方案的命门,它挂了,整个事务体系瘫痪。要解决单点又得给协调者做高可用,复杂度翻倍。

第三,性能。一次提交要两轮网络往返加多次落盘,吞吐远低于单库事务。

这三个代价决定了 2PC 只适合参与者少、对一致性要求极高的场景,比如金融领域的跨行转账。电商下单这种高并发场景用它,性能会先于正确性崩掉。

4. 最终一致路线,本地事务加消息 ​

最终一致的思路是绕开跨库原子性,把一次跨库操作拆成「各自用本地事务保证」加「用消息通知后续动作」。

以「下单后发积分」为例。不追求下单和发积分在同一次全局事务里,而是让下单服务先在自己的本地事务里完成下单,同时往消息表里插一条「该发积分了」的记录,然后异步把这条消息投递出去,积分服务消费后发积分。

java
@Transactional
public void createOrder(Order order) {
    orderMapper.insert(order);              // 本地事务,插入订单
    messageMapper.insert(                   // 同一个本地事务,插入本地消息表
        new Message("order.created", order.getId()));
}

关键在这两条 INSERT 在同一个本地事务里。事务保证了订单和消息要么都写入要么都不写。订单提交成功,消息记录必然存在,之后投递失败还能从本地消息表捞出来重投。这就把「跨库的一致性」降级成了「本地事务的一致性」加「消息的可靠投递」。

代价是中间态。订单提交成功后、积分发送前,这个时间段里订单存在但积分没发。如果业务能接受这个中间态,最终一致就是正确的选择。

5. TCC 和 Saga ​

TCC 是最终一致的另一种落地。它要求业务方把每个操作拆成 Try、Confirm、Cancel 三个动作,Try 阶段预留资源,Confirm 阶段确认提交,Cancel 阶段释放预留。相比 2PC,TCC 不锁资源,性能更好,但要求业务代码实现三套逻辑,侵入性极强。

Saga 把一次长事务拆成多个本地事务,每个本地事务配一个补偿动作。前一个事务成功就触发下一个,某个失败就从失败点往前依次执行补偿。Saga 和 TCC 的区别在于补偿粒度,Saga 是整段反向操作,TCC 是预留资源的确认和释放。

这两种的共同点是都属于最终一致,都存在中间态,都需要业务代码深度配合。它们不是银弹,是在「强一致不可接受」的前提下,用业务侵入换性能和可用性。

6. 怎么选 ​

判断顺序是先问一致性能不能妥协。

业务能否容忍中间态推荐方案理由
不能,比如转账扣款2PC强一致是硬要求,性能差也得接受
能,大多数电商场景本地消息表实现简单,本地事务保证不丢消息
能,且操作可拆成三段TCC性能比 2PC 好,一致性比消息强
能,且是长流程多步骤Saga每个步骤独立补偿,灵活

结论是明确的,绝大多数业务选本地消息表加最终一致。强一致只在资金、账务这类「中间态就是事故」的场景才值得上。判断错了方向,付出的代价不是性能,是根本无法落地,强一致方案的高复杂度会让普通团队做不出来也维护不住。

7. 相关章节 ​

本地消息表的可靠投递依赖消息不丢,见 消息系统高可用实战,从 Outbox 到死信的完整实现见 消息可靠性闭环。消息重复投递导致的重复扣减,靠 幂等控制 兜底。Spring 生态里分布式事务的框架落地,见 Spring 分布式事务。

Released under the MIT License.