为什么单库事务不够用了

单体时代,一个下单操作涉及订单、库存、账户三张表,都在同一个数据库里,一个本地事务 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 适合资金、库存这类对一致性要求极高、且能接受改造成本的核心链路。