Skip to content

Seata:TM、TC、RM与AT/XA/TCC/Saga全过程

一、学完本页必须会什么

Seata 是分布式事务协调框架,不是一条“自动保证一致”的注解。它支持多种模式,不同模式的资源锁定、提交、补偿和失败恢复完全不同。

学完本页,你应该能够:

  1. 解释 TC、TM、RM 分别运行在哪里、保存什么状态。
  2. 解释 @GlobalTransactional 如何开启全局事务并绑定 XID。
  3. 画出 XID 从入口服务传播到下游服务和数据库分支的过程。
  4. 说明 AT 模式第一阶段中 DataSourceProxy、SQL解析、前后镜像、undo_log、Branch Register 和本地提交的顺序。
  5. 解释 AT 为什么第一阶段会释放数据库行锁,却仍有全局锁语义。
  6. 解释普通 SELECT、SELECT FOR UPDATE 与 AT 全局读隔离的差异。
  7. 说明二阶段 Commit 为什么快,Rollback 为什么要校验后镜像。
  8. 说明脏数据、undo_log、全局锁冲突和 TC 重启怎样恢复。
  9. 解释 Seata XA、TCC、Saga 的执行和适用边界。
  10. 用 XID、branchId、lockKey、undo_log 和事务状态排查生产问题。

二、Seata 的位置:框架不等于模式

模式事务思想Seata 提供的主要能力
AT本地提交 + 数据镜像补偿数据源代理、SQL识别、undo_log、全局锁、TC协调
XA标准 XA 原子提交XAResource接入、分支注册、Prepare/Commit/Rollback协调
TCC业务资源预留TCC拦截、分支注册、Confirm/Cancel调度、事务栅栏集成
Saga本地事务 + 业务补偿状态机、步骤执行、补偿和状态持久化

Seata 不能自动处理任意外部系统。例如短信接口没有回滚能力,第三方支付也不会因为本地 Java 异常自动退款。是否能接入某种模式取决于资源能力和业务语义。

版本提醒:Seata 的配置键、服务端存储模式、Starter 集成、控制台和支持特性会随版本演进。项目必须核对 Seata Client、Server、Spring Cloud Alibaba、注册/配置中心、数据库脚本和序列化方式的官方兼容说明,不能把某个版本示例机械复制到所有项目。

三、TC、TM、RM到底是谁

mermaid
flowchart TD
    A["业务入口方法<br/>TM定义全局事务边界"] --> B["TC事务协调器<br/>保存全局与分支状态"]
    C["订单库DataSourceProxy<br/>RM管理分支"] --> B
    D["库存库DataSourceProxy<br/>RM管理分支"] --> B
    E["优惠券库DataSourceProxy<br/>RM管理分支"] --> B
角色所在位置主要职责关键标识
TCSeata Server 集群开启全局事务、保存状态、注册分支、管理全局锁、推动提交/回滚XID、branchId
TM发起全局事务的应用Begin、Commit、Rollback 全局事务,定义边界XID
RM访问事务资源的应用注册分支、报告状态、申请/释放全局锁、执行分支提交或回滚branchId、resourceId、lockKey

一个应用可以同时扮演 TM 和 RM:订单服务的方法开启全局事务时是 TM;它修改订单库时又作为该数据库分支的 RM。

四、全局事务生命周期

mermaid
flowchart TD
    A["外部调用进入代理"] --> B["GlobalTransactionalInterceptor识别事务注解"]
    B --> C["TM向TC发送Global Begin"]
    C --> D["TC持久化全局会话并返回XID"]
    D --> E["RootContext绑定XID到当前执行上下文"]
    E --> F["业务调用多个RM分支"]
    F --> G{"业务方法结果"}
    G -- "正常" --> H["TM向TC请求Global Commit"]
    G -- "异常/超时规则触发" --> I["TM向TC请求Global Rollback"]
    H --> J["TC按模式推动各分支提交"]
    I --> K["TC按模式推动各分支回滚或补偿"]
    J --> L["解绑XID并返回结果"]
    K --> L

核心点:业务方法正常返回只是 TM 请求全局提交的触发条件。最终是否提交成功还取决于 TC 与所有分支的状态。TM 提交请求超时也不代表全局事务一定回滚,必须按 XID 查询最终状态。

五、XID 怎样传播到下游

XID 是全局事务标识。TM Begin 后将它绑定到当前上下文;远程调用集成层把 XID 放入请求附件或 Header,下游收到后重新绑定,RM 才知道本地 SQL 属于哪个全局事务。

mermaid
flowchart TD
    A["订单服务RootContext持有XID"] --> B["Feign/Dubbo拦截器把XID写入请求"]
    B --> C["库存服务入口过滤器读取XID"]
    C --> D["库存线程RootContext绑定XID"]
    D --> E["DataSourceProxy提交时注册库存分支"]
    E --> F["请求结束清理线程上下文"]

如果 XID 没传播:下游只会提交普通本地事务,全局回滚时 TC 找不到这个分支。常见原因包括自定义 HTTP 客户端未接入拦截器、异步线程丢失上下文、消息消费边界错误、代理或网关删除 Header。

安全上不要允许外部客户端随意伪造 XID。事务上下文应只在可信服务网络和受控拦截器之间传播,并避免把内部协调标识直接当作授权依据。

六、Seata 和 Spring 本地事务边界怎么配合

很多线上 Seata 问题不是 AT 镜像本身出错,而是事务边界设计错了。最常见的误区是:以为 @GlobalTransactional 会替代 Spring 的本地事务,或者以为 @Transactional 会自动变成分布式事务。

先记住边界:

注解或机制管什么不管什么
@GlobalTransactionalTM 向 TC 开启、提交或回滚全局事务,绑定 XID不直接管理数据库连接的本地提交细节
@Transactional当前服务当前数据库连接的本地事务边界不自动让其他服务和数据库一起回滚
DataSourceProxyRM 拦截本地 SQL,生成镜像、undo_log、注册分支不代理绕过数据源的手工连接或外部系统
Feign/Dubbo 拦截器把 XID 传到下游不保证下游一定接入 RM,也不保证业务幂等

典型链路是:

mermaid
flowchart TD
    A["入口方法命中@GlobalTransactional"] --> B["TM向TC Begin并绑定XID"]
    B --> C["进入本地@Transactional方法"]
    C --> D["DataSourceProxy参与本地事务"]
    D --> E["执行业务SQL并生成undo_log"]
    E --> F["本地事务提交前注册分支和全局锁"]
    F --> G["本地事务提交"]
    G --> H["入口方法结束后TM请求全局Commit或Rollback"]

这说明:Seata AT 一阶段仍依赖本地事务提交。全局事务是“跨多个本地事务的协调协议”,不是把数据库本地事务取消掉。

6.1 @GlobalTransactional@Transactional 谁包谁

常见建议是让全局事务边界包住业务编排入口,让具体数据库操作仍处在本地事务里。不要把一个很长的外部 HTTP 调用、MQ 等待、人工审批、文件上传都包在全局事务里。

java
@Service
public class OrderApplicationService {

    private final OrderLocalService orderLocalService;
    private final StockClient stockClient;

    public OrderApplicationService(OrderLocalService orderLocalService,
                                   StockClient stockClient) {
        this.orderLocalService = orderLocalService;
        this.stockClient = stockClient;
    }

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(CreateOrderCommand command) {
        orderLocalService.createOrder(command);
        stockClient.lockStock(command.getOrderNo(), command.getSkuId(), command.getCount());
    }
}

@Service
class OrderLocalService {

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderCommand command) {
        // 本地订单库写入由DataSourceProxy生成undo_log并注册分支
    }
}

这段代码表达的是职责分离:应用服务负责全局编排,局部服务负责本地事务。真实项目还要加幂等键、状态机、唯一约束和下游失败语义。

6.2 自调用为什么可能导致事务不生效

Spring AOP 代理有一个经典坑:同一个类内部 this.xxx() 调用不会经过代理,@Transactional@GlobalTransactional 都可能不按预期增强。

java
public class OrderService {

    public void outer() {
        // 这类内部调用可能绕过代理增强
        inner();
    }

    @Transactional
    public void inner() {
        // 以为开了本地事务,实际可能没有经过代理
    }
}

生产排查时,如果发现“加了注解但没有全局事务/本地事务”,要检查:

  1. 方法是否为 public 且由 Spring Bean 代理调用。
  2. 是否同类自调用绕过代理。
  3. 是否异常被 catch 后吞掉,导致事务拦截器认为正常返回。
  4. rollbackFor 是否覆盖了业务自定义异常。
  5. 数据源是否确实被 Seata 代理。
  6. 下游是否收到 XID。

6.3 为什么不建议把远程慢调用放进本地数据库事务

错误写法常见于:

text
开启本地订单事务
  -> 插入订单
  -> 调用库存服务
  -> 调用优惠券服务
  -> 调用支付预下单
  -> 提交订单事务

问题是本地数据库锁会在远程调用期间一直持有。下游慢、网络超时或重试时,订单库行锁、连接池和线程都会被拖住。

更好的思路是缩短每个本地事务:

mermaid
flowchart TD
    A["全局事务编排"] --> B["订单本地短事务"]
    B --> C["库存本地短事务"]
    C --> D["优惠券本地短事务"]
    D --> E["全局提交或回滚"]

如果流程天然很长,例如支付、物流、审批、外部医院接口采集,不要强行 AT。应优先考虑 Saga、Outbox、事务消息、状态机和补偿对账。

6.4 本地事务提交了,全局事务还能回滚吗

在 AT 模式里,一阶段本地事务确实已经提交了。全局回滚不是数据库物理回滚,而是 RM 根据 undo_log 执行一个新的补偿 SQL,把数据恢复到前镜像。

这就是为什么 AT 必须有:

  1. 前镜像:知道回滚目标。
  2. 后镜像:确认没有人绕过 Seata 改过数据。
  3. undo_log:保存可恢复依据。
  4. 全局锁:限制其他 Seata 全局事务并发改同一行。
  5. 幂等和对账:处理二阶段重试、冲突和人工介入。

如果你不能接受“一阶段数据短暂可见”,或者业务要求数据库层强原子提交,应评估 XA;如果业务流程很长或包含外部系统,应评估 TCC、Saga、Outbox 或事务消息。

七、AT 模式成立的前提

AT 适合 Seata 能正确代理的数据源和支持的关系数据库 SQL。它依赖:

  • 应用使用 Seata DataSourceProxy 或对应自动代理。
  • 业务 SQL 能被识别并生成前后镜像和回滚动作。
  • 表有可定位记录的主键。
  • 每个业务库存在版本匹配的 undo_log 表。
  • 所有需要参与全局写隔离的更新都经过 Seata 代理。
  • 全局事务足够短,热点 lockKey 竞争可接受。

AT 不等于适合任意复杂 SQL、存储过程、跨库 SQL、非关系资源和外部 API。具体 SQL 支持范围必须以使用版本官方文档和集成测试为准。

八、AT 第一阶段完整执行流程

以库存 SQL 为例:

sql
update inventory
set available = available - 2
where sku_id = 1001
  and available >= 2;

AT 分支的关键流程:

mermaid
flowchart TD
    A["业务从DataSourceProxy取得ConnectionProxy"] --> B["Executor识别UPDATE和主键条件"]
    B --> C["查询执行前记录形成before image"]
    C --> D["执行真实业务SQL并检查影响行数"]
    D --> E["按主键查询执行后记录形成after image"]
    E --> F["生成undo_log回滚数据和lockKey"]
    F --> G["RM向TC注册Branch并申请全局锁"]
    G --> H{"全局锁是否获得"}
    H -- "是" --> I["业务数据与undo_log一起本地提交"]
    H -- "否" --> J["回滚本地事务并按策略重试或失败"]
    I --> K["RM报告分支一阶段完成"]

8.1 为什么要前镜像

前镜像保存 SQL 执行前的受影响行。回滚时,Seata根据前镜像构造补偿,使数据恢复到分支开始前的值。

8.2 为什么还要后镜像

回滚前要读取当前数据并与后镜像比较。如果当前数据仍等于后镜像,说明自一阶段提交后没有未受控更新,可以安全恢复前镜像;如果不相等,说明数据被其他事务或人工操作改变,直接覆盖可能抹掉合法新数据。

8.3 为什么undo_log和业务数据必须同一本地事务

如果业务数据提交但 undo_log 没提交,全局回滚时没有恢复依据;如果 undo_log 提交但业务数据回滚,又会留下错误日志。DataSourceProxy 需要让二者同生共死。

8.4 Branch Register 为什么在本地提交前

RM 需要向 TC 注册分支并获得冲突资源的全局锁语义后,才能提交本地事务。否则本地数据已提交后才发现全局锁冲突,就无法通过简单本地回滚撤销已提交事实。

九、lockKey 与全局写隔离

lockKey 用资源和主键标识本分支修改的记录,例如概念上类似:

text
inventory:1001
order:ORDER-9001

具体编码由 Seata 版本和资源管理实现负责,业务不应手工拼接依赖内部格式。

mermaid
flowchart TD
    A["全局事务A注册 inventory:1001"] --> B["TC记录该lockKey属于A"]
    C["全局事务B也注册同一lockKey"] --> D["TC拒绝或让B重试"]
    B --> E["A全局提交或回滚完成"]
    E --> F["TC释放全局锁"]
    F --> D

第一阶段本地事务提交后,数据库行锁已经释放;Seata 全局锁是 TC 维护的逻辑写隔离。另一个受 Seata 管理的全局事务会在注册同一 lockKey 时冲突。

但普通 JDBC、手工连接、未代理服务或 DBA SQL 不会自动检查 TC 全局锁,仍可能改动同一行。这解释了为什么 AT 回滚时必须校验后镜像,也说明 AT 不能约束所有绕过代理的写入。

十、AT 的读隔离:普通SELECT和FOR UPDATE不是一回事

AT 一阶段会本地提交业务数据,因此在全局事务最终结束前,普通本地 SELECT 可能读取到该中间结果。AT 的核心默认能力更偏全局写隔离,不应该简单宣称所有读取都天然达到全局事务级读隔离。

如果业务需要在全局事务语义下锁定读取,可使用受支持的 SELECT ... FOR UPDATE 路径,使 RM 在本地行锁之外检查或获取相关全局锁;遇到其他全局事务持锁时等待/重试,直到可安全读取并锁定。

代价是:

  • 增加 TC 交互。
  • 延长锁等待。
  • 热点记录吞吐下降。
  • 长全局事务更容易超时。

是否支持具体 SELECT、隔离级别和 SQL 形式必须以当前版本文档和测试为准。

十一、AT 二阶段提交为什么通常较快

全局提交时,一阶段业务数据已经本地提交。二阶段不需要再次提交业务行,主要是确认全局成功并清理 undo_log、分支状态和全局锁。

mermaid
flowchart TD
    A["TM请求全局Commit"] --> B["TC持久化全局提交状态"]
    B --> C["TC释放或推进分支全局锁状态"]
    C --> D["异步通知RM清理undo_log"]
    D --> E["清理失败可重试,不应撤销已提交业务"]

“异步删除 undo_log”不代表可以永久不清理。大量残留会占空间、影响扫描和排查,需要监控清理失败、旧记录数量和保留策略。

十二、AT 二阶段回滚全过程

mermaid
flowchart TD
    A["TC通知RM回滚branchId"] --> B["RM开启本地事务并锁定undo_log"]
    B --> C["读取rollback_info中的前后镜像"]
    C --> D["按主键读取当前业务数据"]
    D --> E{"当前数据是否等于after image"}
    E -- "是" --> F["根据before image生成并执行补偿SQL"]
    F --> G["删除/标记undo_log并本地提交"]
    G --> H["向TC报告Branch RolledBack"]
    E -- "否" --> I["发生数据冲突,停止盲目覆盖并告警"]

回滚本身也必须可重入。网络断开后 TC 可能再次通知同一 branchId;RM 要依据 undo_log 和分支状态判断已回滚、待回滚还是异常,而不是重复恢复数据。

十三、undo_log 每个字段解决什么

示意表结构如下,精确 DDL 应使用当前 Seata 版本官方脚本:

sql
create table undo_log (
    branch_id bigint not null,
    xid varchar(128) not null,
    context varchar(128) not null,
    rollback_info longblob not null,
    log_status int not null,
    log_created datetime not null,
    log_modified datetime not null,
    unique key ux_undo_log (xid, branch_id)
);
字段作用
xid找到所属全局事务
branch_id唯一定位数据库分支
context保存序列化器等回滚上下文
rollback_info前后镜像和回滚所需数据
log_status区分正常回滚日志与防悬挂等状态
唯一键防止同一全局分支产生重复回滚日志

不能手工删除不理解的 undo_log。它可能是正在进行或待恢复事务的唯一回滚依据。

十四、回滚时后镜像不一致怎么办

示例:

text
before: available=10
AT一阶段后 after: available=8
绕过Seata的操作把它改为7
全局回滚到来

若直接恢复 available=10,会把后来合法扣减 1 覆盖掉。正确策略不是“为了自动回滚强行改回去”,而是:

  1. 保留 XID、branchId、表、主键和镜像证据。
  2. 停止无限自动覆盖。
  3. 查明哪条未受控写入改变了数据。
  4. 根据业务流水计算正确结果。
  5. 通过审计的补偿或人工流程修复。
  6. 修复所有绕过 DataSourceProxy 的写入口。

这类冲突说明隔离边界被破坏,不能只当作“undo_log异常”。

十五、AT 为什么会有额外成本

一次 UPDATE 相比普通本地事务可能增加:

  • SQL 解析。
  • 前镜像 SELECT。
  • 后镜像 SELECT。
  • undo_log 序列化和 INSERT。
  • 向 TC 注册分支和全局锁网络交互。
  • 二阶段清理或回滚。

批量、大范围更新会产生更大镜像和更多 lockKey。热点库存行会导致全局锁冲突。长事务会让冲突窗口扩大。因此 AT 应用于短、小、可代理、热点可控的事务,不应把报表、批量迁移或长外部调用包进全局事务。

十六、AT 最小业务 Demo 与真正发生的事情

java
@Service
public class OrderApplicationService {
    private final OrderRepository orderRepository;
    private final InventoryClient inventoryClient;

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void create(CreateOrderCommand command) {
        orderRepository.insert(command.getOrderNo(), command.getUserId());
        inventoryClient.deduct(
                command.getOrderNo(), command.getSkuId(), command.getCount());
    }
}

下游本地事务:

java
@Transactional(rollbackFor = Exception.class)
public void deduct(String orderNo, Long skuId, int count) {
    int updated = inventoryRepository.deductIfEnough(skuId, count);
    if (updated != 1) {
        throw new BizException("库存不足");
    }
}

注解背后发生的是:

  1. 全局事务拦截器向 TC Begin。
  2. XID 绑定并传播。
  3. 订单和库存数据源代理各自建立 AT 分支。
  4. 每个分支保存镜像和 undo_log,并注册 lockKey。
  5. 业务正常时 TM 请求全局提交;异常时请求回滚。
  6. TC 根据持久化状态推动所有分支收敛。

业务仍要用 orderNo 唯一约束和状态机。若客户端因超时再次使用新订单号调用,全局事务框架无法判断这是同一次业务意图。

十七、Seata XA 模式全过程

XA 模式让数据库 XAResource 保持未最终提交的分支:

mermaid
flowchart TD
    A["TM向TC Begin并传播XID"] --> B["RM开启XA分支并执行SQL"]
    B --> C["分支结束并进入Prepare"]
    C --> D["数据库持久化可恢复状态"]
    D --> E{"TC全局决议"}
    E -- "提交" --> F["RM执行XA Commit"]
    E -- "回滚" --> G["RM执行XA Rollback"]

与 AT 对比:

维度ATXA
一阶段业务数据本地提交,可按镜像补偿Prepared,尚未最终提交
回滚依据undo_log 前后镜像数据库 XA 恢复信息
本地锁释放后由TC全局锁协调Prepared资源通常继续保留锁/上下文
SQL要求必须被AT正确解析和代理依赖数据库/驱动XA支持
性能一阶段快提交但有镜像与全局锁多阶段和持锁成本更明显

XA 适合短事务和强原子提交需求,不适合把慢 HTTP、人工审批和长业务流程放在 Prepared 状态。

十八、Seata TCC 模式与事务栅栏

Seata TCC 将业务 Action 注册成分支,TC 根据全局决议调用 Confirm 或 Cancel。业务必须显式实现资源语义:

java
public interface InventoryTccAction {
    boolean tryReserve(BusinessActionContext context,
                       String orderNo, Long skuId, int count);

    boolean confirm(BusinessActionContext context);

    boolean cancel(BusinessActionContext context);
}

事务栅栏概念上按 xid + branchId + actionName 保存状态:

mermaid
flowchart TD
    A["Try到达"] --> B{"是否已有ROLLBACKED记录"}
    B -- "有" --> C["拒绝迟到Try,防悬挂"]
    B -- "无" --> D["原子预留资源并记录TRIED"]
    E["Cancel到达"] --> F{"是否有Try记录"}
    F -- "无" --> G["记录ROLLBACKED,形成空回滚屏障"]
    F -- "有" --> H["条件释放资源并改ROLLBACKED"]
    I["Confirm到达"] --> J["仅将TRIED条件推进为COMMITTED"]

Confirm/Cancel 可能被 TC 多次重试,必须根据状态幂等返回。业务资源预留与栅栏记录应在同一本地事务中,否则屏障状态和真实库存可能分叉。

深入学习:TCC 专栏 · TCC/Saga 幂等

十九、Seata Saga 状态机

Saga 模式将正向 ServiceTask、补偿任务、分支和状态转换定义为状态机。状态机引擎负责记录执行位置,使进程重启后能从持久化状态继续,而不是依赖 Java 调用栈。

mermaid
flowchart TD
    A["状态机实例开始"] --> B["执行创建订单并持久化结果"]
    B --> C["执行预订库存并持久化结果"]
    C --> D["执行创建物流单"]
    D --> E{"步骤失败"}
    E -- "是" --> F["进入COMPENSATING"]
    F --> G["按已完成步骤逆序补偿"]
    G --> H["COMPENSATED或人工状态"]
    E -- "否" --> I["SUCCEEDED"]

Saga 的关键不是 JSON 定义,而是:

  • 每个步骤和补偿是否幂等。
  • 参数和结果是否足够恢复。
  • 不可逆步骤是否后置。
  • 补偿失败是否有重试、告警和人工接管。
  • 业务查询是否能解释中间状态。

深入学习:Saga 专栏

二十、TC 持久化和高可用为什么重要

TC 保存全局会话、分支会话、锁和最终决议。若只存在单进程内存,TC 宕机会让事务恢复失去依据。生产需要使用当前版本支持并适合的持久化模式,部署多个 Server,并通过注册中心或明确地址让客户端发现。

高可用设计要核对:

  • 多 TC 实例是否访问同一可靠状态存储。
  • Server 节点下线和客户端重连行为。
  • 事务表、锁表、历史清理和索引。
  • 数据库连接池和热点 XID/lockKey。
  • 集群时钟、超时扫描和恢复任务。
  • 注册中心、配置中心和认证凭据。
  • 序列化方式和滚动升级兼容性。

TC 集群可用不等于所有事务一定快速完成。数据库分支不可达、undo 冲突和业务 TCC Cancel 永久失败仍需人工收口。

二十一、超时、重试和状态未知

全局超时是协调器决定进入回滚或超时处理的策略,不表示所有参与者在同一毫秒已经回滚。TC 要逐个通知分支,失败后继续重试。

TM 调用 Commit 超时:

  • TC 可能尚未收到请求。
  • TC 可能已经记录 Commit 决议但响应丢失。
  • 部分分支可能已完成二阶段。

因此调用方应该记录 XID 和业务键,查询全局状态及业务事实,不能直接用新业务号重做。

RM 的分支重试必须有边界。永久数据冲突、SQL不支持、Schema变化不是简单网络瞬时错误,无限重试只会刷日志和加重数据库压力。

二十二、Seata 生产排查 Runbook

22.1 全局事务长时间不结束

  1. 用业务键找到入口日志中的 XID。
  2. 在 TC 状态存储/日志中查全局状态和最后更新时间。
  3. 列出 branchId、resourceId、分支状态和失败原因。
  4. 判断卡在 Begin、Branch Register、Global Commit、Rollback 还是二阶段重试。
  5. 查对应应用实例是否存活、XID 是否正确传播。
  6. 查数据库锁等待、连接池、undo_log 和网络。
  7. 分类瞬时错误、结果未知、永久业务错误和数据冲突。

22.2 Branch Register 或全局锁冲突

按 lockKey 找持有 XID,检查其全局事务是否长时间运行;再查热点主键、长远程调用和重试频率。不要只把锁重试次数调大,否则会把热点争用变成更多等待线程。

22.3 AT 回滚失败

mermaid
flowchart TD
    A["定位XID和branchId"] --> B["确认undo_log是否存在且可反序列化"]
    B --> C["比较before、after和当前数据"]
    C --> D{"当前数据是否等于after"}
    D -- "是" --> E["检查补偿SQL、主键、权限和本地锁"]
    D -- "否" --> F["定位绕过代理的写入,停止盲目覆盖"]
    E --> G["修复后按协议重试回滚"]
    F --> H["按业务流水人工计算和审计修复"]

22.4 有业务数据但没有undo_log

检查:

  • 数据源是否真正被代理。
  • SQL是否在全局XID上下文内。
  • 是否使用了手工原始 DataSource/Connection。
  • undo_log DDL、权限、表名和版本是否正确。
  • 业务是否在存储过程或未支持路径中修改数据。

这是高风险隔离缺口,不能只补一条 undo_log 假数据。

22.5 TC 重启后事务恢复缓慢

检查状态存储连接、恢复扫描、事务表索引、待重试数量、分支应用可达性和数据库锁。大量历史未清理事务会拖慢恢复;需要归档和清理策略,但只能删除已确认终态且满足保留要求的数据。

二十三、监控指标与告警

指标价值
active_global_transactions当前活跃全局事务
begin/commit/rollback TPS事务流量和决议趋势
commit/rollback latency P95/P99协调时延和尾部问题
branch_register_failuresRM注册、网络或锁问题
global_lock_conflicts热点写竞争
rollback_retry_count回滚恢复健康度
dirty_undo_conflicts绕过代理或并发覆盖风险
old_undo_log_count二阶段清理积压
timed_out_transactions超时预算或下游问题
TC store latency/errors协调状态持久化健康度

日志至少关联 XID、branchId、businessKey、resourceId、global/branch status 和 traceId;敏感数据必须脱敏。

二十四、四种模式怎样选择

场景倾向原因
两个关系库、短事务、SQL简单、热点低评估AT低侵入,但验证SQL与全局锁
数据源支持XA且必须原子提交评估XA数据库负责Prepare恢复,成本较高
库存/余额/券有自然冻结模型TCC业务预留语义明确,可控制隔离
履约、审批、物流等长流程Saga不长期持锁,通过补偿收敛
积分、通知、ES同步Outbox/事务消息不需要全局事务阻塞主链路
秒杀热点库存通常不首选AT全局锁热点,考虑队列、原子预扣和专用模型
外部支付/短信Saga/消息/对账外部系统通常不能加入AT/XA

选择前先看分布式事务总览选型与排查

二十五、安全、配置和变更治理

  • TC、注册中心、配置中心必须开启认证和最小权限。
  • 不在仓库中明文保存数据库密码、Seata Server凭据和密钥。
  • XID、事务上下文 Header 只允许可信服务传播。
  • undo_log 可能包含业务行镜像,应按敏感数据标准保护访问、备份和日志。
  • 滚动升级前验证 Client/Server/序列化/数据库脚本兼容。
  • 超时、重试和锁参数通过压测灰度,不在线直接大幅调整。
  • 手工事务修复必须审批、记录前后值和关联工单。

二十六、JDK 8 Demo:后镜像校验为什么必要

java
public class AtImageCheckDemo {
    static final class Image {
        final int before;
        final int after;

        Image(int before, int after) {
            this.before = before;
            this.after = after;
        }
    }

    static int rollback(Image image, int current) {
        if (current != image.after) {
            throw new IllegalStateException(
                    "dirty data: expected after=" + image.after
                            + ", current=" + current);
        }
        return image.before;
    }

    public static void main(String[] args) {
        Image image = new Image(10, 8);
        System.out.println("safeRollback=" + rollback(image, 8));
        try {
            rollback(image, 7);
        } catch (IllegalStateException conflict) {
            System.out.println(conflict.getMessage());
        }
    }
}

输出:

text
safeRollback=10
dirty data: expected after=8, current=7

Demo 只是镜像校验原理。真实 Seata 会保存表、主键、字段值、序列化上下文并在本地事务中执行补偿。

二十七、常见错误与后果

错误后果正确方向
加注解后认为所有远程资源都回滚未加入RM的服务仍本地提交验证XID传播和资源接入
AT事务中调用长外部接口全局锁窗口和超时扩大缩短事务,外部步骤改Saga/消息
AT用于热点秒杀行lockKey冲突严重专用库存模型、队列或TCC
普通SQL绕过DataSourceProxyTC无法建立分支,回滚冲突统一数据源入口并扫描手工连接
宣称AT所有读都全局隔离普通SELECT可能读到一阶段提交值明确读语义,必要时受控FOR UPDATE
手工删除undo_log恢复依据丢失先按XID/branchId取证和协议恢复
TCC Confirm/Cancel不幂等重复扣减或重复释放事务栅栏和条件状态更新
Saga补偿等于数据库回滚忽略退款等新业务事实明确业务补偿和不可逆步骤
TM提交超时就用新业务号重试可能重复全局业务记录XID,查询事实和最终状态
TC单节点且状态仅内存宕机后无法可靠恢复集群和可靠持久化存储

二十八、面试标准回答

28.1 Seata AT 第一阶段做什么

TM开启全局事务并传播XID;每个RM通过DataSourceProxy拦截业务SQL,查询前镜像,执行SQL,再查询后镜像,生成undo_log和lockKey;提交前向TC注册分支并申请全局锁,成功后将业务数据与undo_log在同一本地事务提交。任何需要全局协调的下游都必须收到XID并经过资源代理。

28.2 AT 为什么要前后镜像

前镜像用于生成回滚后的原值;后镜像用于回滚前校验当前数据是否仍等于一阶段提交结果。若不相等,说明有绕过全局锁的写入或人工改数,直接覆盖前镜像会丢失新数据,因此应停止盲目回滚并人工核对。

28.3 AT 全局锁和数据库行锁有什么区别

一阶段本地提交后数据库行锁释放,TC仍记录lockKey形成全局写隔离,其他受Seata管理的全局事务注册相同资源时冲突。未经过Seata代理的普通SQL不会自动检查这把全局锁,所以AT不能约束所有外部写入。

28.4 AT 是否天然提供全局读隔离

不能简单这样说。因为一阶段业务数据已本地提交,普通SELECT可能看到全局事务尚未最终结束的中间值。需要更强读锁语义时可评估受支持的SELECT FOR UPDATE全局锁检查路径,但会增加TC交互和锁等待。

28.5 Seata四种模式怎么选

简单关系库短事务且SQL受支持、热点低可评估AT;资源支持XA且要求原子提交可评估XA;库存、余额、优惠券有自然预留模型适合TCC;履约、审批、物流等长流程适合Saga;积分、通知、ES同步通常更适合Outbox或事务消息,不必强行放进Seata全局事务。

二十九、关联知识点

本章小结

理解 Seata 不能停在 @GlobalTransactional。真正的执行链是 TM Begin、XID传播、RM分支注册、模式特有的一阶段处理、TC持久化决议以及可重试的二阶段恢复。AT 的核心是数据源代理、前后镜像、undo_log和全局锁;XA依赖数据库Prepare;TCC依赖业务预留和栅栏;Saga依赖持久化状态机和补偿。生产落地必须同时设计业务幂等、超时未知、对账、监控和人工处置。