单体拆成微服务后,原本一个本地事务就能搞定的「下单 + 扣库存 + 扣余额」变成了跨三个服务的网络调用。这篇文章源于我们交易链路的一次真实选型过程,把四种主流方案放在一起对比,并给出最终的落地选择。
先明确:我们要的是什么级别的一致性
分布式事务方案没有银弹,只有 trade-off。选型前先回答两个问题:业务能不能容忍中间态被短暂读到(如库存先扣、订单后建)?失败时是要求自动回滚,还是允许事后补偿对齐?交易链路我们要求最终一致、不允许超卖、允许秒级延迟——这直接排除了强一致的 2PC。
四种方案横向对比
- 2PC / XA:强一致,但协调者单点 + 同步阻塞持有数据库锁,高并发下吞吐雪崩,互联网场景基本弃用。
- TCC:Try-Confirm-Cancel 两阶段业务补偿,一致性较好、阻塞短,但每个接口要写三个方法,侵入性极强,空回滚与幂等悬挂都要自己处理。
- 本地消息表:业务表与消息表同库同事务写入,后台任务投递 MQ,消费者幂等处理。实现简单、性能最好,但只保证最终一致,且不适合需要回滚的场景。
- Seata AT:基于全局锁 + undo_log 的自动补偿,业务代码零侵入,一致性优于消息表,代价是额外的解析与日志写入开销。
Seata AT 的实际落地
AT 模式的精髓是「一阶段提交、二阶段异步回滚」:业务 SQL 执行前先解析生成 before/after image 存入 undo_log,全局提交时直接删日志,回滚时用日志反向补偿。业务方只需要一个注解。
1@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)2public Order createOrder(OrderRequest request) {3 // 1. 创建订单(本地事务,注册分支事务)4 Order order = orderMapper.insert(request.toOrder());5 // 2. 扣减库存(RPC,分支事务自动传播)6 stockClient.deduct(request.getSkuId(), request.getCount());7 // 3. 扣减余额(RPC)8 accountClient.debit(request.getUserId(), request.getAmount());9 return order;10}1112// 每个参与库都需要一张 undo_log 表,Seata 服务端(TC)独立部署接入时踩过两个坑:一是全局锁冲突——热点商品并发扣减时在 TC 侧排队,我们的做法是把库存扣减从 AT 分支里挪出去,改用「库存服务本地事务 + 失败时订单侧主动回滚」;二是 undo_log 表与业务表必须同库,分库场景要逐个库建表,漏建会导致回滚静默失败。
分布式事务的第一原则是「能不用就不用」:先用业务手段把跨库写操作收敛到单库,剩下的真正跨服务场景再按一致性要求选方案。
最终选型与建议
- 交易主链路(下单-余额):Seata AT,要求较高一致性且能接受约 15% 的额外耗时。
- 库存扣减:服务内本地事务 + 预扣模型,避开全局锁热点。
- 积分、消息通知等非核心链路:本地消息表 + MQ,最终一致即可。
- TCC 仅在对账、资金清算等强一致且并发不高的场景保留。
上线半年,交易链路没再出现过超卖与账务不一致。回头看,方案本身只占三成,真正的功夫在链路梳理:哪些写操作必须原子、哪些可以异步对齐,想清楚这个,选什么框架都是水到渠成。