前言
在微服务架构下,业务会拆分到多个独立服务,跨服务操作就会遇到分布式事务问题,保证多个微服务的数据一致性。Seata 是目前主流的分布式事务框架,本文重点拆解 Seata 的三大核心组件 TC/TM/RM,AT模式完整执行时序、锁模型,补充 TCC 模式,以及 AT/XA/TCC/Saga 的适用场景对比,同时梳理开发中高频踩坑点。
Seata AT模式是业务开发最常用方案,本文案例均基于Seata。
一、Seata 三大核心组件:TC、TM、RM
Seata 架构由三个角色组成:TC事务协调者、TM事务管理器、RM资源管理器。
1. TC(Transaction Coordinator,事务协调者,Seata Server)
TC是独立部署的服务端进程,需要单独下载 seata-server 包启动。
- 职责:存储全局事务、分支事务状态;接收TM/RM注册与状态上报;驱动所有分支事务执行二阶段提交/回滚,是全局事务的总指挥中心。
- 存储模式:
- file 文件存储:TC重启事务数据丢失,仅用于Demo测试
- db数据库存储:将全局事务、分支事务、锁信息存入
global_table/branch_table/lock_table,测试环境常用 - raft集群模式:生产推荐,TC多节点集群,leader宕机自动选主,解决TC单点故障
- ⚠️单点故障问题:
TC宕机后,新的全局事务无法创建;已经开启但未完成的全局事务,二阶段提交/回滚无法执行,分布式事务卡住。
补充:已经完成一阶段提交的本地数据不会自动回滚,等TC服务恢复后,TC会定时扫描未完成事务,继续执行二阶段流程。
2. TM(Transaction Manager,事务管理器,客户端组件)
TM不是独立进程,是嵌入业务微服务的客户端组件,引入seata-spring-boot-starter就自带该能力。
- 职责:开启全局事务(
@GlobalTransactional),标记全局事务边界;业务执行完毕后,通知TC全局事务提交或回滚。 - 特点:一次全局事务中,只有一个TM,也就是发起分布式事务的服务。
3. RM(Resource Manager,资源管理器,客户端组件)
RM同样是客户端组件,嵌入所有参与分布式事务的微服务。发起事务的服务可以同时是TM+RM。
- 职责:管理本地数据库资源;向TC注册分支事务,上报分支执行状态;接收TC下发二阶段指令,执行本地分支提交或回滚(AT模式依靠undo_log实现补偿回滚)。
一句话通俗类比:
TC = 中央调度指挥部;RM = 各个业务节点(订单、库存);TM = 发起本次跨多节点操作的请求入口。
为什么不能让TM兼任TC?3个核心原因
1.TM所在业务服务随时可能宕机(最核心)
订单服务作为TM,如果同时充当TC。一阶段全部执行成功,准备执行二阶段时订单服务宕机。事务状态保存在业务服务内,库存、账户分支事务一阶段已经提交,无人触发二阶段,undo_log和锁无法清理,造成数据不一致。
独立TC场景:事务状态持久在TC,即使TM业务服务宕机,TC仍然存活,定时扫描未完成事务,等下游服务在线后继续下发二阶段指令,保证事务闭环。
核心原则:分布式事务状态必须保存在独立第三方,不能放在参与事务的业务节点上。业务节点会重启、下线、扩容。
2.TM只是事务参与者之一,无法拥有全部分支视图
一次事务链路:订单(TM)→库存(RM)→账户(RM)。TM只清楚自己本地分支情况,无法实时感知其他分支执行状态。网络分区场景下,TM收集信息不全,无法判断全局提交/回滚。
TC作为公共中心,所有RM上报状态,TC持有全部分支事务完整视图,统一仲裁全局结果。
3.业务服务频繁上下线,不适合承担状态持久与调度工作
订单、库存这类业务微服务,会频繁发布重启、弹性扩缩容,不适合长期保存事务状态,也不适合长时间重试调度。TC专门负责状态持久、定时扫描、重试调度,独立部署高可用集群,实现职责解耦。
和经典2PC对照:Seata TC就是原生2PC中的协调者;TM/RM属于参与者侧组件。如果TM兼任TC,就是参与者充当协调者,分布式系统中单点风险极高。
TM的存在意义:不能直接删掉TM,只留TC+RM
TC只会被动接收指令,保存事务状态,下发指令。TC不会主动感知业务什么时候开启事务。TM负责事务入口工作:
- 业务代码走到
@GlobalTransactional方法,TM调用TC创建全局事务,拿到XID; - XID放入调用链路,传递给下游所有微服务;
- 业务执行完成,TM向TC上报全局事务投票结果(提交/回滚);业务抛异常,通知TC全局回滚。
如果没有TM:没有组件申请全局事务、生成XID、传递XID;下游RM不知道当前属于哪个全局事务,无法注册分支。RM仅在收到XID后,才能注册分支、执行二阶段指令。
类比:TC是裁判,RM是球员,TM是教练。教练告诉裁判比赛开始,比赛结束上报结果;只有裁判+球员,比赛无法启动。
重要提醒:TM不是独立服务。引入seata starter的微服务自带TM+RM。发起事务的服务激活TM能力;下游被调用服务仅使用RM能力。
二、注解区分:@Transactional VS @GlobalTransactional(高频踩坑)
| 注解 | 归属 | 作用范围 |
|---|---|---|
| @Transactional | Spring原生 | 仅管控当前JVM本地数据库事务,感知不到远程微服务,无法实现分布式事务 |
| @GlobalTransactional | Seata提供 | 开启Seata全局分布式事务,底层封装TM逻辑,生成XID,串联所有RM分支事务 |
SpringBoot中 @EnableTransactionManagement 误区
SpringBoot自动装配DataSourceTransactionManagerAutoConfiguration,只要引入数据库相关依赖,SpringBoot自动注册事务管理器,自动开启AOP事务代理。
- SpringBoot项目:
@EnableTransactionManagement可省略,属于冗余注解,加不加不影响@Transactional本地事务生效; - SSM原生Spring项目:必须手动添加
@EnableTransactionManagement开启事务AOP。
对Seata的@GlobalTransactional来说,启动类加不加@EnableTransactionManagement不影响全局事务。@GlobalTransactional会创建独立Seata事务切面,不依赖Spring原生事务AOP;但是分支内部仍然依赖Spring本地事务管理器。
✅工程标准写法(AT模式)
业务链路:A服务 → B服务 → C服务
- A:请求入口,分布式事务发起方,添加@GlobalTransactional,同时具备TM+RM能力
- B:A远程调用的中间服务,仅RM,只需要@Transactional
- C:B远程调用的下游服务,仅RM,只需要@Transactional
场景1:前端下单请求,链路 A→B→C
// A服务(TM+RM)
@Service
public class OrderService {
// 全局事务入口,只用@GlobalTransactional,不要叠加@Transactional
@GlobalTransactional
public void createOrder(){
// 本地创建订单(RM分支)
orderMapper.insert();
// 远程调用B
bFeign.deduct();
}
}// B服务(RM)
@Service
public class AccountService {
// 分支方法,保留Spring原生@Transactional
@Transactional
public void deduct(){
// 扣余额,远程调用C
cFeign.reduceStock();
}
}// C服务(RM)
@Service
public class StorageService {
@Transactional
public void reduceStock(){
// 扣库存
}
}场景2:后台管理接口,直接访问B服务,链路 B→C
此时B变成链路最外层入口,B上添加@GlobalTransactional,B作为TM;下游C依旧只保留@Transactional。这是一套独立全局事务,和场景1A发起的事务互不干扰。
强制编码规范(避坑)
- 全局入口方法,不要同时写@Transactional + @GlobalTransactional,切面优先级混乱,虽然偶尔能跑,禁止混用。入口只用
@GlobalTransactional; - 下游RM分支必须保留@Transactional。AT模式一阶段,业务SQL和undo_log写入必须在同一个本地事务,保证原子性;不加会导致undo_log写入失败,回滚失效;
- 仅链路最外层入口添加@GlobalTransactional,下游微服务不能添加
@GlobalTransactional。下游添加会新建独立全局事务,不是同一个事务; - 一个调用链路只能有一处
@GlobalTransactional,多处会创建多个独立全局事务,引发异常。
三、AT模式完整执行时序(修正:先TM创建全局事务,再执行业务分支)
AT模式整体分为两大阶段:一阶段(准备)、二阶段(提交/回滚,TC驱动)
整体前置流程
- 调用A服务
@GlobalTransactional方法,TM向TC申请创建全局事务,生成全局唯一XID; - XID放入请求头,随远程调用传递到下游B、C;B、C拿到XID后激活RM;
- 各个服务RM执行本地分支事务(下面一阶段时序),每个分支独立执行下面步骤;
单分支一阶段完整时序(全部在同一个本地事务内,未commit)
- RM解析SQL,查询数据前镜像;
- 执行业务SQL(update/insert/delete);
- 查询数据后镜像;
- 插入undo_log回滚记录;
- RM调用TC注册分支事务,申请全局锁;
- 拿锁失败:直接回滚本地事务,业务SQL、undo_log全部撤销;
- 拿到全局锁成功,执行本地事务commit;
- commit本地事务后,MySQL原生行锁释放,RM上报TC:分支一阶段执行成功。
核心设计目的:先生成undo_log,再注册分支,规避空回滚问题。
如果反过来:先注册分支拿锁,再执行业务SQL。SQL执行报错,TC已经记录分支,但RM没有undo_log,后续全局回滚时找不到回滚数据,触发空回滚异常。
同时,lockKey需要执行SQL拿到主键,构造表名+主键传给TC申请锁,未执行SQL前无法获取主键。
二阶段(TC统一调度)
TC收集所有分支一阶段结果,做全局仲裁:
- ✅ 所有分支一阶段成功:TC下发提交指令给全部RM;RM收到指令,删除undo_log,二阶段完成。
- ❌ 任意分支失败:TC下发回滚指令给全部RM;RM收到回滚指令后,先校验当前数据库真实数据:
- 如果数据库数据没有被其他事务修改:直接使用undo_log前镜像执行回滚,恢复数据,完成后删除undo_log;
- 如果数据库数据已经被其他业务修改(出现脏写冲突):不能直接覆盖数据,Seata会抛出异常,终止自动回滚流程,交由业务设置对应的人工处理/告警逻辑,防止覆盖第三方更新的数据,造成数据错乱。
⚠️重点面试考点:
AT二阶段回滚不是无脑覆盖,会先比对当前库数据与undo_log后镜像。一旦检测到数据被外部修改,自动回滚不再执行,避免强行覆盖别人已提交的数据。这种场景属于数据冲突,需要业务监控告警、人工介入处理。
四、并发场景:XA vs Seata AT锁阻塞差异
XA(传统2PC)
一阶段执行SQL,加MySQL行锁,事务不commit。行锁和数据库连接一直占用,直到二阶段TC下发commit/rollback才释放。
- 阻塞位置:数据库内核层面;
- 阻塞后果:长事务长时间占用数据库连接,连接池容易耗尽,数据库压力大;
- 锁持有时间:整个分布式事务生命周期。
Seata AT全局锁阻塞
T2执行update、生成undo_log之后,在RM调用TC申请全局锁这一步被拦截。
T2业务SQL在本地事务内部执行成功,MySQL行锁短暂持有,但还没有commit。拿到锁失败直接rollback,本次修改不会持久到数据库。
- 阻塞位置:业务代码和TC之间(应用层逻辑判断);
- 阻塞特点:默认快速失败,不阻塞等待;锁重试需要手动配置参数开启;
- 配置参数:
seata.client.lock.retryTimes、seata.client.lock.retryInterval
⚠️风险:开启长时间锁重试,T2会持续持有MySQL行锁,占用数据库连接,丧失AT高性能优势,类似XA长锁问题。
| 对比项 | XA MySQL行锁阻塞 | Seata AT TC全局逻辑锁阻塞 |
|---|---|---|
| 阻塞位置 | MySQL数据库内 | TC应用层 |
| 阻塞时是否执行update | ✅已执行update,数据库锁等待 | ✅事务内执行update,commit前拦截 |
| 是否长期占用数据库连接 | 是 | 默认不会(快速失败) |
| 回滚代价 | 需要回滚已执行SQL | 拿锁失败直接回滚,代价小 |
五、AT模式隔离级别:脏读、脏写
AT模式默认读未提交(Read Uncommitted)
假设T1一阶段完成,本地事务commit,库存更新为98,TC全局锁还持有,等待二阶段。
1.普通select查询(无@GlobalTransactional)
不受Seata管控,直接读取数据库已提交数据,可以读到库存=98。如果后续T1二阶段回滚,就出现脏读。
2.Seata全局事务T2,默认隔离级别
T2普通select同样读到脏数据;如果T2尝试update这一行,申请全局锁会被拦截,最终回滚,AT默认一定防止脏写。
3.手动开启Seata读已提交 @GlobalTransactional(isolation = Isolation.READ_COMMITTED)
T2查询时RM主动访问TC校验全局锁,如果该行存在全局锁,RM读取undo_log前镜像,返回修改前旧数据,避免脏读。
代价:查询增加一次TC远程调用+读取undo_log,性能下降。
总结:AT默认:允许脏读,禁止脏写;读已提交是应用层实现,非数据库原生隔离。
六、TCC模式(新增)
TCC 介绍
TCC(Try-Confirm-Cancel),是Seata支持的业务补偿型分布式事务,无数据库undo_log,完全由业务代码编写三段接口,属于侵入业务代码的方案。
TCC同样基于TC/TM/RM架构,TC负责全局事务状态保存与二阶段调度。
三个阶段:
- Try(预留资源,一阶段):检查资源,冻结/预留业务资源,不直接扣减真实数据。
例:扣余额Try:校验余额,冻结余额,不扣实际账户余额 - Confirm(确认提交,二阶段成功分支):确认执行业务,使用Try阶段预留资源,正式提交。
例:Confirm:把冻结的余额真正扣掉 - Cancel(回滚补偿,二阶段失败分支):释放Try阶段预留资源,恢复数据。
例:Cancel:解冻余额,恢复冻结额度
一句话概括:Try做资源预留;Confirm确认提交;Cancel撤销释放。
TCC完整流程
- TM收到
@GlobalTransactional,向TC创建全局事务,拿到XID;XID传递下游; - 所有参与分支执行Try,预留资源,向TC注册分支;
- TC收集所有Try结果:
- 全部Try成功:TC下发Confirm指令,所有分支执行Confirm,完成业务提交;
- 任意Try失败:TC下发Cancel指令,所有分支执行Cancel,释放预留资源。
TCC优缺点
✅优点:
- 不依赖数据库undo_log,不绑定数据库,可以用于非数据库资源(调用第三方支付、外部API);
- 无AT的脏读问题,隔离性由业务自行控制;
- 没有全局锁,高并发场景性能可控。
❌缺点:
- 业务侵入极强:每一个业务操作,必须手动编写Try/Confirm/Cancel三套接口,开发工作量巨大;
- 需要处理幂等、空回滚、悬挂三大经典TCC异常问题,开发门槛高;
- 空回滚:Cancel先于Try执行;
- 悬挂:Try晚于Cancel执行;
- 幂等:Confirm/Cancel重复调用,不能重复执行业务;
- 业务逻辑复杂,维护成本高。
适用场景:
跨第三方服务、非数据库资源、高性能核心链路,AT无法满足的场景。
七、Saga事务模式(长事务场景方案)
AT、XA不适合长事务(人工审批、履约流程)。AT虽然释放MySQL行锁,但TC全局锁会一直持有到二阶段结束,如果等待人工审批几天,全局锁占用多天,其他分布式事务无法修改该行,业务卡死。
Saga核心思想
将一个大全局事务拆分成一串独立本地子事务。每一步子事务执行完,立刻提交本地事务,释放数据库锁;后续步骤失败,执行配套反向补偿接口。全程不持有长锁。
案例:请假审批流程
创建请假单(本地事务提交,锁释放)→等待主管人工审批(等待几天,无任何锁)→审批通过执行人事登记;审批拒绝,执行补偿接口修改请假单状态为作废。
Saga两种实现模式
- 编排式(Choreography):服务之间互相发MQ消息,无中心协调器;缺点:链路复杂,维护困难。
- 编排式(Orchestration):存在中心Saga协调器驱动各个子服务,不一定依赖MQ。
Saga优缺点
✅优点:无长锁,适配人工审批、外部回调这类长事务场景;
❌缺点:
- 每个正向业务接口,必须手写配套补偿接口,开发成本高;
- 无隔离性,子事务提交后,数据立刻对外可见,中间态业务数据持久落库;
- 补偿接口执行时可能失败,需要人工对账兜底。
AT/XA追求尽量对外不可见中间状态;Saga天生阶段性提交,中间数据对外可见,仅保证最终一致性。业务开发需要识别中间状态,不能把流程未完成的数据当成有效数据。
八、XID传递(重要前置条件)
Seata依靠XID串联整个分布式事务链路。XID需要放在请求头跨微服务传输。
- Feign:seata starter自动装配拦截器,自动传递XID,开箱即用;
- RestTemplate:必须手动配置拦截器,否则XID无法传递,下游RM识别不到全局事务,分支不注册,异常无法回滚。
XID丢失后果:下游服务只是独立本地事务,出现数据不一致。
九、模式横向对比总结
| 模式 | 原理 | 锁特点 | 适用场景 | 业务侵入 |
|---|---|---|---|---|
| AT | undo_log自动补偿 | 一阶段释放MySQL行锁,TC全局锁到二阶段结束 | 微服务短事务,数据库场景,最常用 | 低,几乎不用写额外业务代码 |
| TCC | Try/Confirm/Cancel三段业务接口 | 业务层资源预留,无框架全局锁 | 对接第三方API、非数据库资源、高性能链路 | 极高,需要手写3套接口,处理空回滚/悬挂/幂等 |
| Saga | 分步本地事务+反向补偿 | 无长锁 | 长事务,人工审批、履约流程 | 中,编写补偿接口 |
| XA | 数据库原生2PC | 长持有数据库行锁 | 强一致性,并发低 |
总结
- TC是独立部署服务,全局事务状态存储与二阶段调度中心;TM/RM是客户端组件嵌入业务微服务;
- AT模式执行顺序:TM创建全局事务、生成XID → 各RM执行一阶段(镜像+undo_log+注册分支)→ TC收集结果驱动二阶段提交/回滚;二阶段回滚会校验当前数据库数据,数据未修改直接回滚;数据已被修改,抛出冲突异常,等待业务告警人工处理;
- AT适合短事务高性能场景;XA适合强一致性场景,锁占用时间长;TCC适合对接外部系统,侵入业务;Saga专门用于长事务、人工审批场景;
- 编码规范:链路入口加
@GlobalTransactional,下游分支保留@Transactional;保证XID跨服务传递;禁止链路多处开启全局事务。