为什么单库事务不够用了
单体时代,一个下单操作涉及订单、库存、账户三张表,都在同一个数据库里,一个本地事务 BEGIN/COMMIT 就保证了要么全成、要么全回滚。拆成微服务之后,订单、库存、账户各自有库,一次下单要跨三个服务三次远程调用,没有任何一个本地事务能同时管住这三个库。这就是分布式事务要解决的问题。
核心矛盾很现实:跨网络调用必然有失败、超时、重复,而业务又要求"扣了库存就得扣钱、扣钱失败就得回滚库存"。业界没有银弹,只有几种在一致性、性能、侵入性之间做不同取舍的模式。
- 2PC两阶段提交的本质与阻塞代价。
- Seata AT无侵入、靠 undo log 补偿的自动模式。
- TCCTry/Confirm/Cancel 手写三步的强一致。
- 事务消息基于 MQ 的最终一致轻量方案。
两阶段提交的本质与代价
两阶段提交(2PC)是最朴素的思路:先让所有参与方"预提交"并锁住资源,等协调者收到全部成功再统一"提交",任意一个失败就全部"回滚"。它的一致性最强,但问题也很明显:同步阻塞。预提交之后资源被锁住,协调者一旦宕机,所有参与方就一直卡在那等。
所以真正的生产系统很少裸用 XA 2PC。它更像是所有分布式事务模式的思想起点——后面的 AT、TCC、Saga,都是在"怎么减少阻塞、怎么降低侵入"上做文章。
Seata AT 模式:无侵入的补偿
Seata 的 AT 模式对业务最友好:你照常写本地 SQL,Seata 通过代理数据源在你执行前后各拍一次数据快照,生成一份 undo log。全局事务提交时正常走;一旦某个分支失败,协调者通知各方用 undo log 把数据反向回滚回去。
@GlobalTransactional // 一个注解开启全局事务
public Order create(Order o) {
orderMapper.insert(o); // 本地事务,自动被代理
stockFeign.deduct(o.getSku()); // 远程库存
accountFeign.pay(o.getMoney()); // 远程账户
}
AT 的代价是要靠全局锁防止脏写,且回滚依赖前后快照一致。它适合大多数普通业务,几乎不用改代码,是中小团队的首选。
TCC:手写三步的强一致
TCC 把每个服务拆成三个接口:Try(预留资源)、Confirm(确认提交)、Cancel(释放回滚)。以扣款为例,Try 不是真扣钱而是把钱"冻结",Confirm 才真正扣,Cancel 则解冻。因为全程不锁数据库行,阻塞小、一致性强,但要为每个业务手写三个接口,侵入性最高。
// 账户服务要提供三个接口
try: freeze(money) // 冻结金额,不实际扣
confirm: deduct(frozen) // 真正从冻结里扣
cancel: unfreeze(money) // 失败则解冻
TCC 适合资金、库存这类对一致性要求极高、且能接受改造成本的核心链路。
Saga:长事务的正向与补偿
Saga 面向长流程(比如跨多个步骤的报销、审批):先一路正向执行,每一步都配一个对应的补偿动作。任何一步失败,就把已经执行过的步骤倒序补偿回去。它没有全局锁、性能好,但只保证最终一致,且业务要为每一步写补偿逻辑。
事务消息:最终一致的轻量解
很多场景其实不需要强一致,只要"最终能对上"。事务消息(RocketMQ 事务消息、本地消息表)思路是:本地事务和"发消息"绑在一起成功,下游消费消息执行下一步,消费失败就重试,直到所有环节都落地。它不跨服务锁资源,靠重试和对账兜住最终一致,是最常用、最轻的方案。
几种模式怎么选型
经验法则:
- 普通业务、想少改代码 → Seata AT。
- 资金/库存核心链路、要强一致 → TCC。
- 跨多步的长流程、能接受最终一致 → Saga。
- 异步解耦、吞吐优先 → 事务消息 / 本地消息表。
别一上来就上重型框架。能用事务消息解决的,别用 TCC;能用 AT 的,别手写 XA。
落地清单
- 先判断业务是要强一致还是最终一致。
- 普通业务优先 Seata AT,注意全局锁与快照回滚。
- 核心资金链路用 TCC,为每个接口写好 Try/Confirm/Cancel。
- 异步链路用事务消息 + 消费重试 + 定时对账兜底。
- 所有模式都要考虑空回滚、幂等与悬挂,不能只看 happy path。