今年帮一个团队复盘线上事故时,我发现一个特别普遍的误区:一提微服务事务,很多人的第一反应就是"上 Seata",而且所有业务都往同一个模式里塞。结果订单这种强一致场景用 AT 模式被全局锁卡死,积分这种能容忍延迟的业务也压着 undo_log 不释放,更别提一堆压根不需要分布式事务的纯查询接口也被套了一层代理。这不是 Seata 的问题,是事务分级没做好。
微服务事务分级治理这件事,本质上是先把业务的一致性需求按强度拆开,再在 Seata 的 AT/TCC/SAGA/XA 四种模式里找到对应的方案,最后把数据库侧的分布式事务能力(比如 TDSQL)当作硬兜底。这篇把我自己从架构设计到线上排障的一整套经验写下来,给准备做分布式事务改造、或者已经在 Seata 里被各种问题折腾过的团队参考。
1. 先想清楚一个核心问题:业务到底需要什么粒度的事务
1.1 微服务拆出来的分布式事务困境
在单机单库时代,一个用户下单的流程,扣库存、写订单、加积分在同一个数据库事务里,ACID 由数据库保证。微服务一拆,问题就来了:这三个操作落在三个服务里,每个服务各自的数据库连接、各自的本地事务,原本"要么全成功、要么全失败"的语义被彻底打破了。
我拿最常见的下单场景说:库存服务扣减成功,订单服务创建失败。从数据库层面看,库存表那条更新已经提交了,订单表没有插入,两边都"成功"或"失败"了,但业务整体上是一个半成功状态。如果再叠加积分、优惠券、物流通知这些环节,失败组合会呈指数级增长:扣库存成功、订单失败、积分成功、通知失败……每一个组合都要写一套对账和补偿逻辑,业务代码根本撑不住。
所以分布式事务解决方案解决的是一个权衡问题:为了跨服务的原子性,你愿意付出多少性能、代码复杂度和运维成本。Seata 也好、TDSQL 也罢,它们不是在"有事务"和"没事务"之间做选择,而是在"什么业务值得开一个全局事务"上做选择。想明白这一点,才会理解为什么不能一套方案打天下。
1.2 一致性需求分级:L1 强一致、L2 最终一致、L3 尽力而为
我在实际项目里习惯把事务需求分成三个等级,这个分级是后续所有方案选型的基础。
| 级别 | 典型业务 | 一致性要求 | 推荐方案 |
|---|---|---|---|
| L1 强一致 | 扣减库存、账户扣款、优惠券锁定 | 秒级内必须一致,失败必须回滚或精确补偿 | Seata AT/TCC/XA,或数据库级分布式事务 |
| L2 最终一致 | 订单状态流转、用户积分、发货通知 | 允许短暂不一致,最终必须收敛到正确状态 | 可靠消息 + 幂等消费,或 Seata SAGA |
| L3 尽力而为 | 操作日志、埋点统计、异步聚合 | 允许丢失或明显延迟 | MQ 异步任务,无需事务中间件 |
为什么 L1 不用消息最终一致?因为"最终一致"意味着从失败到收敛之间存在时间窗口。库存扣减漏了或者扣错了,哪怕只差一秒,资金损失也是实实在在的;但积分少加两条,用户可能过几天才感知,补发完全来得及。业务的容忍度不同,技术方案的等级自然不同。
1.3 分级治理的第一个收益:把成本花在刀刃上
做过 Seata 的人都有体会,全局锁和分支事务快照是有实际开销的。AT 模式一阶段要写 undo_log,注册分支事务到 TC,二阶段提交前全局锁一直持有,热点数据行上的并发会被强制串行化。
我用同样规格的数据库压过同一张订单表:单机本地事务的 TPS 能到 2000+,一旦引入 Seata AT 模式的全局锁和分支注册,热点行并发下 TPS 直接掉到 300~400。当然这是数量级的参考,不同环境和数据分布会有差异,但方向是一致的——分布式事务的开销是真实的、可测量的。
分级治理的意义就在这里:如果把 L2、L3 的业务从 Seata 里请出去,用消息队列和异步任务处理,热点链路上清掉一大半不必要的事务负担,真正把全局事务的代价留给 L1 里那些必须强一致的业务。这不是技术保守,是架构取舍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Seata 模式选型:AT/TCC/SAGA/XA 各自的战场
2.1 AT 模式:无侵入的甜与全局锁的毒
AT 模式是很多团队入门 Seata 的第一站,原因无他:对业务代码侵入极低。你只需要在方法上加一个 @GlobalTransactional 注解,Seata 通过代理数据源自动完成分布式事务的协调。
原理拆开来说并不复杂:
- 一阶段:业务 SQL 和 undo_log 的写入在同一本地事务中提交,同时向 TC(Transaction Coordinator)注册分支事务。undo_log 里记录的是数据变更前后的镜像。
- 二阶段提交:所有分支事务都成功,异步删除 undo_log。
- 二阶段回滚:有分支失败,根据 undo_log 里的前后镜像反向生成补偿 SQL,恢复原数据。
java复制@GlobalTransactional(name = "create-order", timeoutMills = 30000)
public Order createOrder(OrderDTO orderDTO) {
// 扣减库存
stockService.deductStock(orderDT
