分布式事务
下单要同时扣库存、扣优惠券、生成订单,三个动作落在三个服务各自的数据库里。单库事务的
BEGIN和COMMIT管不到别的库,任何一个动作失败,已经成功的动作没法自动回滚。分布式事务要解决的就是这件事,一次跨多个库的操作,怎么保证要么全成功要么全失败。
1. 单库事务为什么失效
单库事务靠的是数据库的原子性。扣库存、扣优惠券、插订单在同一个库、同一个事务里,数据库保证这三条要么一起生效要么一起回滚。这个保证成立的前提,是它们都在一个数据库实例的一个事务里。
拆成微服务后,前提没了。库存服务、优惠券服务、订单服务各有一个库,三个库各自提交各自的事务,没有一个全局事务能同时管住它们。
下单流程变成三段独立提交。库存扣了,优惠券扣了,订单插入时失败。前两个库已经提交,回不去了。用户没下单成功,库存和优惠券却少了。这就是核心矛盾,本地事务管不了跨库的原子性。
2. 两条路线的分水岭
所有分布式事务方案都逃不开一个取舍,一致性和性能只能二选一。强一致要求每一步都等所有参与者确认,代价是阻塞和性能损耗。最终一致允许中间态存在,靠后续补偿把状态追平,代价是短时间内数据对不上。
取舍的依据只有一个问题,业务能不能容忍中间态。下单扣库存这类操作,容忍不了「库存扣了但订单没生成」的中间态,才需要强一致。而电商里大多数场景其实能容忍,订单和库存短暂不一致,靠后续对账补平即可。
想清楚这一点,就不会一上来就上强一致。强一致方案复杂度高、性能差,大多数业务根本用不到。
3. 强一致路线,2PC 及其代价
2PC 引入一个协调者,把提交拆成两个阶段。第一阶段问所有参与者「能不能提交」,第二阶段根据回答决定「都提交」还是「都回滚」。
协调者 参与者A 参与者B
│ ① 询问能否提交 │ │
├──────────▶ │ │
│ ├── ② 都回复 Yes ──▶ │
│ ③ 通知提交 │ │
├──────────▶ │ │2PC 保证了一致性,但代价有三条。
第一,阻塞。参与者回复 Yes 后,就处于等待协调者指令的状态,锁着资源不放。协调者一旦宕机,所有参与者卡在中间状态,事务悬挂。
第二,单点。协调者是整个方案的命门,它挂了,整个事务体系瘫痪。要解决单点又得给协调者做高可用,复杂度翻倍。
第三,性能。一次提交要两轮网络往返加多次落盘,吞吐远低于单库事务。
这三个代价决定了 2PC 只适合参与者少、对一致性要求极高的场景,比如金融领域的跨行转账。电商下单这种高并发场景用它,性能会先于正确性崩掉。
4. 最终一致路线,本地事务加消息
最终一致的思路是绕开跨库原子性,把一次跨库操作拆成「各自用本地事务保证」加「用消息通知后续动作」。
以「下单后发积分」为例。不追求下单和发积分在同一次全局事务里,而是让下单服务先在自己的本地事务里完成下单,同时往消息表里插一条「该发积分了」的记录,然后异步把这条消息投递出去,积分服务消费后发积分。
@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 分布式事务。