目录
什么是分布式事务
Seata 三大核心角色
工作流程
Seata 的四种模式
一、AT 模式(默认,最常用,自动事务)
AT 模式原理(两阶段)
Seata AT 完整实战案例
步骤 1:undo_log 建表
步骤 2:Maven 依赖
步骤 3:配置文件
步骤 4:order‑service 代码(TM + RM)
4.1 RestTemplate 配置
4.2 OrderService 业务代码
步骤 5:下游 account‑service、storage‑service 公共 Filter
📝完整执行流程,带 undo_log 变化演示
阶段 1:TM 开启全局事务,生成 XID
阶段 2:order‑service 本地插入订单(它同时也是 RM)
阶段 3:RestTemplate 调用 account‑service 扣余额
阶段 4:RestTemplate 调用 storage‑service 扣库存
分支 A:全部正常
分支 B:异常
重点故障演示:如果没有写下游 SeataXidFilter 会发生什么?
关键注意点
面试提问参考
关键注意点
对比 Feign 与 RestTemplate
一句话总结
二、TCC 模式
三、SAGA 模式(长事务,适合长流程)
四、XA 模式
Seata 架构部署
SpringBoot 微服务依赖
AT 模式常见坑(面试高频)
面试经典问题
场景 1:同步 RPC(order‑service → account‑service → storage‑service)
场景 2:MQ 异步
方案 1:Seata‑TCC(手动编码)
方案 2:Seata‑SAGA
方案 3:RocketMQ 事务消息(半消息)
方案 4:手动传递 XID,强行 AT(不推荐)
各个模式适合什么场景
面试高频问题
Q:为什么异步 MQ 不能直接用 Seata AT?
Q:那什么时候 AT 不能用?
Q:TCC/SAGA/ 事务消息和 AT 的区别?
一句话总结
什么是分布式事务
分布式事务:一次业务操作,跨多个数据库 / 多个微服务,要保证所有数据库操作要么全部成功,要么全部回滚。
例子:
下单业务:
order‑service创建订单、account‑service扣余额、storage‑service扣库存。三个属于不同微服务、不同数据库。
如果订单创建成功,扣钱成功,扣库存失败 → 数据不一致,产生脏数据。
本地事务(MySQL 事务)只能管同一个数据库,管不了跨库跨服务,就需要分布式事务框架Seata。
Seata 三大核心角色
| 角色 | 全称 | 作用 |
|---|---|---|
| TC | Transaction Coordinator 事务协调者 | 独立服务,Seata‑Server。维护全局事务状态,指挥所有分支提交 / 回滚。 |
| TM | Transaction Manager 事务管理器 | 业务应用端,开启 / 结束全局事务,向 TC 申请全局事务 ID (XID)。 |
| RM | Resource Manager 资源管理器 | 业务应用端,管理本地事务,向 TC 上报分支事务状态,执行分支提交 / 回滚。 |
工作流程
- TM向 TC 申请一个全局事务 XID(全局唯一事务 ID)
- TM 开启全局事务,执行业务;各个微服务(RM)执行业务 SQL,分支事务注册到 TC,XID 会沿着 RPC 调用链路传递。
- 所有分支执行完成:
- 全部成功:TM 通知 TC,TC 通知所有 RM 提交本地事务。
- 任意分支失败:TM 通知 TC,TC 通知所有 RM 回滚本地事务。
XID = 全局事务 ID,整个分布式事务的唯一编号。
一次全局事务,从头到尾只有同一个 XID,所有参与这个分布式事务的微服务、分支事务,全部都要带上这个 XID 上报给 TC。
TM在事务入口拿到 XID,当 A 服务调用 B 服务(Feign/RPC 等远程调用)的时候,要把这个 XID 传给下游 B 服务;B 服务拿到 XID 之后,自己作为 RM 向 TC 注册分支事务。 这就是:XID 沿着 调用链路传递。
Seata 的四种模式
一、AT 模式(默认,最常用,自动事务)
全称:Automatic Transaction,无侵入,业务几乎不用改代码,基于undo_log 回滚日志。 前提:数据库支持 ACID(MySQL)。
AT 模式原理(两阶段)
第一阶段:执行业务 SQL,不提交,记录 undo_log
- RM 执行业务 update/insert/delete;
- 记录 undo_log:保存修改前镜像 (before image) 和 修改后镜像 (after image);
- 本地事务提交;释放数据库锁;把分支事务状态上报 TC。
⚠️第一阶段就提交本地事务!数据库锁释放,性能好。
第二阶段:根据 TC 指令
- ✅提交:直接删除对应 undo_log,不用再操作数据库。
- ❌回滚:读取 undo_log 里 before_image,把数据还原成修改前的值,然后删除 undo_log。
📌必须建表:每个业务库都要执行
undo_log表。
建表 SQL(MySQL 示例)
Seata 官方提供了标准建表脚本,每个参与 AT 模式事务的业务库都要执行一次:
CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';容易混淆的点
| 表 | 建在哪里 | 是否手动建 |
|---|---|---|
undo_log | 每个业务库 | 必须手动建 |
global_table/branch_table/lock_table/distributed_lock | Seata Server(TC)端的数据库 | 需手动建(TC 启动时也不会自动建,除非用了某些存储模式的初始化脚本) |
小结
- AT 模式下,每个业务库都必须有
undo_log表,否则分支事务注册时会报错。 - 官方只提供 SQL 脚本,不会自动执行,需要你自己在业务库里跑一遍。
- 如果用的是TCC / Saga / XA 模式,则不需要
undo_log表 —— 这张表是 AT 模式独有的。
⚠️注意区分和MySQL InnoDB 的 undo log
MySQL InnoDB 的 undo log Seata 的 undo_log表是什么 InnoDB 存储引擎内部的事务日志 用户业务库里的一张普通业务表 谁管理 MySQL 引擎自己管理,对用户透明 Seata 客户端(RM)读写,MySQL 只把它当普通表 存在哪里 系统表空间( ibdata1)或独立 undo 表空间(undo_001、undo_002)你的业务数据库里,一张可见的表 需不需要建 不需要,MySQL 初始化时自动创建 需要,因为它就是一张用户表 用途 支持事务回滚、MVCC(快照读) 记录数据修改前的镜像,供 Seata 分布式事务回滚用
AT 模式问题:脏写问题,Seata 用全局锁解决。
全局锁:第一阶段更新数据时,向 TC 登记行锁;其他事务不能修改同一行,避免覆盖。
使用 AT 模式代码极其简单:
1. Feign调用的代码:只要在入口方法上加@GlobalTransactional,其余微服务不需要额外注解,Feign 自动传递 XID。
@GlobalTransactional // ✅开启全局事务,TM标记 public void createOrder(){ // 1.本地保存订单 orderMapper.insert(order); // 2.远程调用扣余额(account‑service RM) accountFeign.deduct(userId,money); // 3.远程调用扣库存(storage‑service RM) storageFeign.deduct(goodsId,count); }2. 使用 RestTemplate 实现(⚠️RestTemplate不会自动传递 XID,需要手动处理请求头)
Feign 有 Seata 内置拦截器自动透传 X‑XID;RestTemplate 没有,必须手动把 XID 放到 HTTP 请求头;下游服务也要写过滤器从请求头取出 XID 绑定到
RootContext。
Seata AT 完整实战案例
业务架构
- Nacos:同时作为 Seata‑Server 注册中心 + 微服务注册中心
- Seata‑Server(TC):注册到 Nacos,集群名
default,存储模式 db(生产) - order‑service:TM,同时也是 RM;本地插入订单;RestTemplate 调用下游。数据库:
order_db - account‑service:RM;扣用户余额。数据库:
account_db - storage‑service:RM;扣商品库存。数据库:
storage_db
重点:RestTemplate不会自动传递 XID,需要发送端增加拦截器,下游服务增加 Filter 解析绑定 XID。 全部业务库使用新版官方 undo_log 表。
步骤 1:undo_log 建表
每个业务库执行 undo_log 建表(order_db /account_db/storage_db 都执行)
CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime(微秒)', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime(微秒)', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table'; ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);⚠️ Seata‑Server 自己的库不需要 undo_log;undo_log 只在业务数据库。
⚠️重要:每个参与分布式事务的数据库,都要执行建
undo_log表 SQL。
步骤 2:Maven 依赖
三个微服务都需要
<!-- seata starter --> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.7.1</version> </dependency> <!-- nacos注册发现 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>步骤 3:配置文件
以 order‑service application.yml 举例;account、storage 只改 application‑id
spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 seata: application-id: order-service tx-service-group: my_tx_group service: vgroup-mapping: my_tx_group: default # 和seata-server集群名称对应 registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: public group: SEATA_GROUP步骤 4:order‑service 代码(TM + RM)
4.1 RestTemplate 配置
增加拦截器自动塞入 X‑XID 请求头
import io.seata.core.context.RootContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.web.client.RestTemplate; import java.util.Collections; @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); ClientHttpRequestInterceptor interceptor = (request, body, execution) -> { String xid = RootContext.getXID(); if(xid != null){ // Seata固定请求头常量 X‑XID request.getHeaders().add(RootContext.KEY_XID, xid); } return execution.execute(request, body); }; restTemplate.setInterceptors(Collections.singletonList(interceptor)); return restTemplate; } }4.2 OrderService 业务代码
import io.seata.spring.annotation.GlobalTransactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; // TM:开启Seata全局事务 AT模式 @GlobalTransactional public void createOrder(Long userId, Long goodsId, Integer count, Integer money) { // 1.本地事务:插入订单(当前order‑service,RM) Order order = new Order(); order.setUserId(userId); order.setGoodsId(goodsId); order.setCount(count); order.setMoney(money); orderMapper.insert(order); // 2.RestTemplate远程调用 account‑service 扣余额 String accountUrl = "http://account-service/account/deduct?userId=" + userId + "&money=" + money; restTemplate.getForObject(accountUrl, String.class); // 3.RestTemplate远程调用 storage‑service 扣库存 String storageUrl = "http://storage-service/storage/deduct?goodsId=" + goodsId + "&count=" + count; restTemplate.getForObject(storageUrl, String.class); // 模拟异常:如果抛出异常,全局事务回滚 // int i = 1 / 0; } }关键点:
@GlobalTransactional标记当前服务为TM,向 TC 申请全局 XID,XID 保存在RootContextThreadLocal。 RestTemplate 拦截器把 XID 放到 HTTP HeaderX‑XID,发给 account‑service、storage‑service。
步骤 5:下游 account‑service、storage‑service 公共 Filter
两个服务都要配置
RestTemplate 调用过来,下游必须配置 Filter,从 http 请求头取出 X‑XID,绑定到当前线程 RootContext;否则下游没有 XID,不会加入全局事务,分布式事务失效!
import io.seata.core.context.RootContext; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class SeataXidFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpReq = (HttpServletRequest) request; String xid = httpReq.getHeader(RootContext.KEY_XID); boolean bind = false; if(xid != null){ RootContext.bind(xid); bind = true; } try{ chain.doFilter(request,response); }finally { // web线程池复用,必须解绑,防止旧XID污染后续请求 if(bind){ RootContext.unbind(); } } } }account‑service Controller 示例
@RestController @RequestMapping("/account") public class AccountController { @Autowired private AccountMapper accountMapper; @GetMapping("/deduct") public String deduct(Long userId, Integer money){ // Filter已经把XID绑定当前线程,当前服务作为RM accountMapper.deductBalance(userId,money); return "扣余额成功"; } }storage‑service 的 Controller 逻辑完全一样。
📝完整执行流程,带 undo_log 变化演示
初始数据:
- order_db:order 表空
- account_db:user_id=1001,balance=1000
- storage_db:goods_id=2001,count=100
调用入口:orderService.createOrder(1001,2001,2,200)
阶段 1:TM 开启全局事务,生成 XID
@GlobalTransactional生效,order‑service (TM) 向 TC 申请全局事务 ID。示例:
XID = "127.0.0.1:8091:40001"XID 存入 order‑service 当前线程
RootContext。
阶段 2:order‑service 本地插入订单(它同时也是 RM)
- 执行
orderMapper.insert(order);Seata RM 代理数据源拦截 insert。 - 查询 before_image(插入前没有数据),after_image 就是新插入订单的数据。
- 同一个本地事务:insert 订单 + insert undo_log(order_db 的 undo_log)
branch_id=5001,xid=127.0.0.1:8091:40001,log_status=0- 提交本地 MySQL 事务!order 表已经有这条订单,释放锁。
- RM 向 TC 注册分支事务(branch_id=5001)。
阶段 3:RestTemplate 调用 account‑service 扣余额
- RestTemplate 拦截器读取
RootContext.getXID(),放入 Http 请求头X‑XID:127.0.0.1:8091:40001。 - HTTP 请求发到 account‑service。
SeataXidFilter拦截请求,取出 header 中的 XID,执行RootContext.bind(xid)绑定到 account 服务工作线程。- 执行
accountMapper.deductBalance(1001,200)。- RM 查询 before_image:balance=1000
- 执行 update,balance 变成 800
- 同一个事务插入 account_db.undo_log,branch_id=5002,log_status=0
- 提交本地事务,余额已经 800,释放锁。
- account‑service RM 向 TC 注册分支 branch_id=5002。返回 http 响应。
阶段 4:RestTemplate 调用 storage‑service 扣库存
- 同样 HTTP 请求头携带同一个 XID。
- storage 服务 Filter 绑定 XID。
- 扣库存,storage_db 插入 undo_log,branch_id=5003,log_status=0。
- 提交本地事务,向 TC 注册分支 branch_id=5003。返回。
此时三个数据库 undo_log 都有 log_status=0 记录;所有本地事务全部提交完成。TC 上记录全局 XID 以及 3 个 branch_id。
分支 A:全部正常
没有异常(注释掉 int i=1/0)
- createOrder 方法执行完毕,TM 通知 TC:全局事务提交。
- TC 向 3 个 RM 下发 commit 指令。
- 每个 RM 收到 commit:直接删除自己库中对应 xid+branch_id 的 undo_log 记录。
- 全部 undo_log 清空。
✅最终状态:订单创建成功,余额 800,库存扣减成功。
分支 B:异常
打开模拟异常int i = 1 / 0;,方法抛出异常
- 代码抛出算术异常。
@GlobalTransactional捕获异常,TM 通知 TC:全局事务回滚。 - TC 向 3 个 RM 下发 rollback 指令。
- order‑service RM:读取 order_db 的 undo_log,拿到 before/after 快照,执行补偿删除订单,删除 undo_log。
- account‑service RM:读取 account_db undo_log,恢复 balance=1000,删除 undo_log。
- storage‑service RM:读取 storage_db undo_log,恢复库存,删除 undo_log。
✅最终:订单消失、余额回到 1000、库存恢复,全部回滚,数据一致。
重点故障演示:如果没有写下游 SeataXidFilter 会发生什么?
下游请求拿不到X‑XID,RootContext.getXID() == null。
- account、storage 执行 SQL,不会注册分支事务到 TC,不会写入 undo_log。
- 抛出异常触发全局回滚:order‑service 订单被回滚删除;但是扣余额、扣库存已经提交,数据不一致!
关键注意点
- RestTemplate 必须发送端拦截器、接收端 Filter 成对出现;Feign 不需要,seata 自动装配。
- Filter 中
finally必须unbind();线程池不复用清理,旧 XID 残留会污染后续普通请求。 - undo_log 每个业务库独立建表,不能只建一张。
- Seata‑Server 生产必须 db 存储模式,不能 file 模式,否则重启丢失事务状态,二阶段无法处理。
@GlobalTransactional必须加在业务入口方法;内部方法调用不加 AOP 不会生效。
面试提问参考
Q:上面例子,一阶段各个 RM 就已经提交本地事务了,为什么还能回滚? A:依靠 undo_log 快照保存修改前数据,二阶段做补偿 SQL,属于补偿式分布式事务,不需要持有数据库长锁。
Q:XID 是怎么从 order‑service 传递到 account‑service? A:RestTemplate 拦截器把 XID 放入 HTTP 请求头 X‑XID;下游 Filter 取出绑定到 RootContext。
关键注意点
- 为什么 Feign 不用写 Filter?Seata starter 自动注册 Feign 拦截器 + 接收端的拦截器;RestTemplate 没有内置接收处理,下游必须手写 Filter 绑定 XID。
- finally 一定要执行
RootContext.unbind()web 容器是线程池,线程会复用。不 unbind,旧 XID 残留在线程,后面普通请求会错误加入旧全局事务,产生严重 bug。 - 测试:手动在代码写
int i=1/0;制造异常。
- 开启
@GlobalTransactional:全部回滚,订单插入、扣余额、扣库存全部撤销。 - 如果把 Filter 注释掉,下游拿不到 XID:本地订单回滚,但是扣余额扣库存提交成功,数据不一致。
对比 Feign 与 RestTemplate
表格
| 方式 | 发送方 | 接收方 |
|---|---|---|
| Feign | Seata 自动拦截器添加 X‑XID | Seata 自动拦截器解析 X‑XID,无需编码 |
| RestTemplate | 需要自己写拦截器添加 X‑XID | 自己写 Filter 解析绑定 XID |
一句话总结
⚠️注意:RestTemplate 只是普通 HTTP 客户端,Seata 没有为它做适配;需要我们自己在请求发送时把 XID 塞入请求头;下游服务从请求头取出 XID 绑定到 RootContext,RM 才能加入全局事务。
二、TCC 模式
手动编码,T‑Try C‑Confirm C‑Cancel
TCC:Try、Confirm、Cancel,完全由业务代码自己写逻辑,没有 undo_log 表。 适合不支持 AT 的场景,比如非数据库资源(调用外部接口、Redis)。
三段式:
- Try:预留资源。检查、冻结资源(冻结余额、冻结库存),不真正扣减。
- Confirm:确认执行。全部 Try 成功,真正执行业务(真正扣钱扣库存)。
- Cancel:取消回滚。任意 Try 失败,释放预留资源,解冻。
每个分支都要写 Try/Confirm/Cancel 三套业务代码,侵入性高,开发量大。 Seata-TCC 框架负责调度这三个方法,业务实现逻辑。
@LocalTCC public interface AccountTCCService { @TwoPhaseBusinessAction(name = "deduct",commitMethod = "confirm",rollbackMethod = "cancel") void tryDeduct(@BusinessActionContextParameter(paramName = "userId") Long userId, @BusinessActionContextParameter(paramName = "money")BigDecimal money); boolean confirm(BusinessActionContext ctx); boolean cancel(BusinessActionContext ctx); }坑:幂等!Confirm/Cancel 可能重复调用,代码必须做幂等处理。
三、SAGA 模式(长事务,适合长流程)
适合长业务流程,比如订单履约、流程耗时很长。 把大事务拆成一串本地事务,每个服务提供正向操作 + 补偿回滚操作。
- 正常:依次执行每个正向服务。
- 某一步失败:逆序执行前面每一步的补偿回滚。
两种实现:
- 注解 Saga(简单)
- 状态机 Saga(复杂流程,写 json 状态定义文件)
没有锁,性能高;没有隔离性,中间数据对外部可见。适合业务可以接受中间状态暴露的长流程。
四、XA 模式
XA 是数据库标准分布式事务协议。
- 一阶段:各个 RM 数据库执行 SQL,prepare,不提交。
- 二阶段:TC 通知全部 commit /rollback。
缺点:一阶段会持有数据库锁直到二阶段完成,锁时间长,性能差。 优势:数据库原生强隔离。一般很少线上使用。
表格
| 模式 | 侵入性 | 性能 | 适用场景 |
|---|---|---|---|
| AT | 低,仅注解 | 高 | MySQL 微服务,绝大多数业务首选 |
| TCC | 高,手写三套代码 | 高 | 非数据库资源、特殊业务 |
| SAGA | 中 | 高 | 长流程业务,允许中间状态可见 |
| XA | 低 | 差 | 强一致,并发低场景 |
企业开发 90% 场景使用 AT 模式。
Seata 架构部署
- Seata‑Server(TC)独立部署
- 各个微服务引入
seata‑spring‑boot‑starter,作为 TM/RM。 - Seata Server 存储模式:
- file:内存,重启丢失数据,仅测试。
- db:把事务日志存在数据库,生产必须用 db 模式。
- 注册中心:Seata Server 注册到 Nacos;微服务通过 Nacos 找到 TC。
SpringBoot 微服务依赖
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.7.1</version> </dependency>application.yml(微服务端)
seata: application-id: order-service tx-service-group: my_tx_group #事务组 service: vgroup-mapping: my_tx_group: default #映射TC集群名 registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: public group: SEATA_GROUPSeata Server 也注册到 Nacos,微服务不用写 TC 的 IP,通过 nacos 发现 TC。
AT 模式常见坑(面试高频)
- 每个业务数据库必须创建 undo_log 表,少一个库就回滚失效。
@GlobalTransactional只加在事务发起入口方法;不能加在内部调用方法上(AOP 失效)。- XID 依靠 Feign 传递;如果是其他调用方式,需要手动传递 XID。
- AT 模式不支持 DDL 语句,只支持 DML update/insert/delete。
- 全局锁:高并发修改同一行,会等待全局锁,出现等待。
- Seata Server 生产环境必须配置 db 存储,不能 file 模式,否则重启丢失事务状态,无法回滚。
- 幂等:二阶段请求可能重试,虽然 AT 框架内部做了,但业务也要注意。
面试经典问题
- AT 模式一阶段为什么提交本地事务?
第一阶段提交本地事务,释放数据库行锁,提升并发性能;依靠 undo_log 做回滚。
- AT 模式和 TCC 区别?
AT 框架自动生成 undo_log,业务几乎无侵入;TCC 全部补偿逻辑业务手写,适合非数据库资源。
- 如果 Seata Server 宕机,已经提交的事务会怎么样?
Seata Server 重启后读取数据库事务日志,继续完成未完成的二阶段提交 / 回滚。
- 什么是空回滚?什么是悬挂?
- 空回滚:分支还没执行 try 业务,收到回滚指令。RM 记录空回滚标记,不执行业务。
- 悬挂:回滚先执行完,再收到分支业务请求。RM 发现有空回滚标记,拒绝执行业务。 Seata AT 已经内置处理空回滚、悬挂。
注意⚠️:
异步 MQ 场景,不能直接简单用 Seata‑AT;但同步 RPC 场景,AT 是首选。不是 AT 不能用分布式事务,而是AT 强依赖 XID 在调用链路透传。
先分清两种场景:
- ✅同步调用(A 服务 Feign/RestTemplate 调用 B,A 等待 B 返回):
Seata AT完全可以用,生产最常用。 - ⚠️异步 MQ(A 发消息到 MQ,B 消费消息,两者没有同步调用链路):不能直接用 AT。
为什么 MQ 异步不能直接 AT? AT 需要 XID 顺着请求链路传递。A 服务 TM 产生 XID,发 MQ 消息之后,A 的本地线程就结束了;MQ 消费是另外一个独立线程、独立请求,没有任何机制自动把 XID 带到消费者那边。 消费者拿到消息时,线程里没有 XID,RM 无法注册分支事务,AT 失效。
场景 1:同步 RPC(order‑service → account‑service → storage‑service)
@GlobalTransactional public void createOrder(){ orderMapper.insert(order); // feign/restTemplate同步远程调用 accountFeign.deduct(); storageFeign.deduct(); }✅AT 模式完美支持,这就是 AT 的标准使用场景。
场景 2:MQ 异步
A 发消息,B 异步消费
示例:order‑service 创建订单,发送 MQ 消息,storage‑service 监听 MQ 消息扣库存。
order‑service(TM) → send MQ消息 → storage‑service异步消费消息扣库存❌直接写下面代码是错误,AT 不生效
@GlobalTransactional public void createOrder(){ orderMapper.insert(order); mqProducer.send(goodsMessage); //发送消息,之后线程结束 // 消息消费者在另一个线程,没有XID,扣库存是普通本地事务! }问题:
@GlobalTransactional只作用在 order‑service 这个调用线程;- MQ 消费者是另一个独立线程,没有 XID;
- 如果扣库存失败,订单不会回滚,数据不一致。
那 MQ 异步场景,有哪些解决方案?
方案 1:Seata‑TCC(手动编码)
- 生产者、消费者都实现 TCC 三段逻辑;
- 生产者 Try 阶段发送预备消息;Confirm 才真正投递消息;Cancel 删除消息。
- 侵入大,要手写 Try/Confirm/Cancel。
方案 2:Seata‑SAGA
把整个业务编排成状态机,每一步定义正向操作 + 补偿操作; 消息消费失败,执行前面业务的补偿回滚。适合长流程。
方案 3:RocketMQ 事务消息(半消息)
RocketMQ 原生事务消息,保证:本地事务执行成功,消息才对外可见。 适合:本地数据库操作 + 发 MQ 消息。
但是它只能保证「本地事务和发消息原子性」,不能管理消费者内部的分布式事务。 也就是:order 库插入订单成功,消息一定发送成功;但是 storage 消费消息扣库存失败,RocketMQ 事务消息帮不了你,需要业务重试 / 死信队列。
方案 4:手动传递 XID,强行 AT(不推荐)
可以手动把 XID 放到 MQ 消息体,消费者拿到消息取出 XID,手动RootContext.bind(xid)。
⚠️大坑!TM(发起全局事务的那个服务)的事务在发送完消息之后,很可能已经结束提交 / 回滚了。 等消费者消费消息的时候,全局事务早就结束了,TC 上已经没有这个 XID 的事务状态,RM 注册分支失败。 👉所以这种方式基本不可行,不建议这么干。
AT 模式的全局事务生命周期,和TM 方法执行的生命周期绑定。
@GlobalTransactional修饰的方法执行完毕,全局事务就结束。MQ 消费者是晚于 TM 方法执行的,时间对不上。
各个模式适合什么场景
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 微服务之间同步 Feign/RestTemplate 调用,都是 MySQL 数据库 | ✅Seata AT | 首选,低侵入 |
| 跨服务异步 MQ,链路断开,时间不同步 | ❌不能直接 AT | 优先 TCC / SAGA / RocketMQ 事务消息 |
| 资源不是数据库,调用外部接口、Redis | ✅TCC | AT 依赖 undo_log 数据库,不适用 |
| 业务流程很长,耗时久,不适合锁等待 | ✅SAGA | 无锁,允许中间状态对外可见 |
面试高频问题
Q:为什么异步 MQ 不能直接用 Seata AT?
- AT 的全局事务生命周期绑定 TM 方法执行;TM 方法执行完成,全局事务就结束。
- MQ 消费是异步晚执行,消费的时候全局事务可能已经结束。
- 没有自动传递 XID 的链路;就算手动把 XID 塞进消息,TC 上该全局事务已经结束,无法注册新的分支事务。
Q:那什么时候 AT 不能用?
- 不是数据库资源(Redis、第三方 http 接口);
- 异步 MQ 解耦场景;
- 有 DDL 语句;
- 数据库不支持 undo_log。
Q:TCC/SAGA/ 事务消息和 AT 的区别?
- AT:框架自动生成 undo_log,适合同步数据库微服务调用;
- TCC:业务手写预留、确认、回滚逻辑,同步异步都可以;
- SAGA:正向 + 补偿,长流程异步业务;
- RocketMQ 事务消息:解决【本地 DB + 发消息】原子性,管不到消费者内部。
一句话总结
Seata AT 不是不能做分布式事务,它只适合「同步调用链路」的分布式事务;一旦变成 MQ 异步,调用链路断裂、时间错位,AT 就不再适用,这时才考虑 TCC、SAGA、事务消息。