Seata
Java 高级 · 阿里中间件
跨服务的事务一致性是分布式系统的硬骨头。Seata 提供 AT、TCC、Saga、XA 多种模式,以低侵入实现分布式事务。本文聚焦最常用的 AT 模式(自动补偿)的底层机制——undo_log、全局锁、两阶段提交,以及它如何保证隔离性;并对比 TCC 的适用边界。
一、三角色与 XID 传播
Seata 的架构由三个角色组成:
- TC(Transaction Coordinator):事务协调者,独立部署的 Seata Server,维护全局事务与分支事务状态,驱动提交/回滚。
- TM(Transaction Manager):事务发起方,标记
@GlobalTransactional的方法,向 TC 申请全局事务 XID、决定最终提交或回滚。 - RM(Resource Manager):资源管理器,每个参与分支事务的服务,向 TC 注册分支、上报状态、执行本地补偿。
XID 如何在微服务间传播? TM 开启全局事务后,XID 存进 ThreadLocal;通过 Seata 的 TransactionPropagationInterceptor(对 Dubbo/Feign/RestTemplate 等适配),在 RPC 调用时把 XID 放进请求头/附件,下游 RM 取出 XID 绑定到自己的 ThreadLocal,从而把一次分布式调用串成同一个全局事务。
二、AT 模式:自动补偿的底层
AT 模式对业务零侵入——你只需在发起方加 @GlobalTransactional,本地事务照常写。它的魔法在"自动生成前后镜像 + 两阶段":
2.1 执行阶段(一阶段,本地事务内)
拦截你的业务 SQL(基于数据源代理 DataSourceProxy → ConnectionProxy),做四件事:
- 解析 SQL:提取表名、条件、操作类型。
- 生成 before image(前镜像):执行前先
SELECT ... FOR UPDATE(加本地行锁)查"修改前的数据快照"。 - 执行业务 SQL。
- 生成 after image(后镜像):查"修改后的数据快照"。
- 写
undo_log:把 before/after 镜像、表名、主键、以及branch_id/xid写进同一个本地事务(和业务数据一起提交)。同时向 TC 注册分支并申请全局锁(全局行锁)。
关键点:一阶段本地事务就提交了(业务数据已落库),不是"等全局提交才提交"。这正是 AT 高性能的原因——各分支尽早释放本地 DB 锁,只保留"全局锁 + undo_log"用于回滚。
2.2 回滚阶段(二阶段,TC 驱动)
全局回滚时,TC 通知各 RM 回滚。RM 取出 undo_log 的 before image,做反向 SQL(insert→delete、update→还原旧值、delete→insert 回原值)补偿。补偿前用 before image 与当前数据做脏写校验:若当前数据≠after image,说明这期间有别的事务改过这行(脏写),无法直接回滚,走重试 + 告警人工介入。
2.3 全局锁:如何避免脏写
AT 的隔离性靠全局锁(global lock):一阶段提交前,RM 向 TC 申请"这行数据的全局锁"(按 表名+主键 维度,TC 内存维护)。只要全局事务没结束,其他想改同一行的 AT 分支事务就会拿不到全局锁而阻塞——从而防止两个全局事务并发改同一行造成的脏写。
- 全局锁由 TC 集中管理,是跨库的"行级互斥"。
- 代价:热点行(如库存扣减)会被全局锁串行化,竞争激烈时吞吐下降、甚至锁等待超时引发连锁回滚。
DANGER
- AT 只保证"最终一致",不是强一致:一阶段各分支已各自提交,全局回滚前存在"部分提交"的短暂窗口(如订单成功、库存回滚中)。资金类强一致场景用 TCC。
- 本地异常被吞:
@GlobalTransactional内try-catch吞掉异常却不抛出,TC 收不到回滚信号,造成部分提交且无法补偿。务必让异常传播到 TM。 - 忘记建
undo_log表:AT 回滚依赖它,缺失则无法补偿,分支提交后无法恢复。各参与库必须建 Seata 官方 DDL。 - 大事务持有全局锁过久:全局锁阻塞其他分支,长事务引发锁等待超时与连锁回滚。
三、TCC 模式:业务层两阶段
TCC 把事务拆成 Try / Confirm / Cancel 三个方法,由业务自己实现:
- Try:资源检查与预留(如冻结库存、预扣金额),不做最终提交。
- Confirm:真正提交(如扣减冻结的库存),必须幂等(可能重试)。
- Cancel:释放预留资源(如解冻库存),也必须幂等。
TCC 不依赖 undo_log,无全局锁,性能高、强一致,但要业务手写三阶段,侵入大。适合资金、库存等"不能丢、不能重"的场景。Seata 通过 @TwoPhaseBusinessAction + TCCResourceManager 注册 TCC 资源,TC 在二阶段驱动 Confirm/Cancel。
四、Saga 与 XA
- Saga:长事务链路,每个服务提供"正向 + 补偿",某步失败则反向调用前面各步补偿。适合跨多个系统、链路长、需要人工干预的流程。
- XA:基于数据库 XA 协议的两阶段提交,强一致但锁资源久、性能差,Seata 也支持(
DataSourceProxyXA)。
五、生产落地
store.mode=db(推荐),TC 集群 + 数据库高可用,避免单机单点;registry/config接 Nacos。- 缩小
@GlobalTransactional范围,非必要不在事务内做远程调用/长 IO(减少全局锁持有时间)。 - 热点行(库存扣减)用 TCC 或"库存预扣 + 最终对账"降低锁冲突。
- 监控
rollback次数、lock_key等待、分支注册耗时,定位慢分支与锁热点。
TIP
AT 的隔离性本质是"本地锁(FOR UPDATE)+ 全局锁(TC 集中)"的双锁配合:本地锁保证一阶段镜像读取的隔离,全局锁保证跨分支的写互斥防脏写。理解这点,你就明白 AT 为什么"快但有最终一致窗口、且怕热点行"。
📚 官方文档与参考资料
- Seata 官方文档:https://seata.io/zh-cn/docs/overview/what-is-seata.html
- Seata GitHub:https://github.com/seata/seata
- Seata AT 模式设计文档:https://seata.io/zh-cn/docs/dev/mode/at-mode.html
✏️ 练习与自测
- 画出 AT 模式下"下单失败"时 TC 如何通知各 RM 用
undo_log回滚的时序,标注一阶段/二阶段边界。 - 说明 AT 的全局锁如何避免脏写,以及它带来的性能代价与热点行问题。
- 什么场景应放弃 AT 改用 TCC?给出判断依据(强一致 vs 最终一致、热点行、侵入度)。
- 若一阶段本地事务已提交,但 TC 在二阶段通知回滚前宕机,如何保证最终回滚?(提示:TC 的事务状态持久化与重试)
本节小结
Seata 以 TC/TM/RM 三角色 + XID 传播实现分布式事务。AT 模式靠"undo_log 自动补偿 + 全局锁防脏写 + 两阶段"实现低侵入,但只保证最终一致;资金等强一致场景上 TCC(业务手写 Try/Confirm/Cancel,无全局锁、强一致、侵入大)。落地三件事:建 undo_log、注解标边界、TC 高可用 + 全局锁治理。