Seata:TM、TC、RM与AT/XA/TCC/Saga全过程
一、学完本页必须会什么
Seata 是分布式事务协调框架,不是一条“自动保证一致”的注解。它支持多种模式,不同模式的资源锁定、提交、补偿和失败恢复完全不同。
学完本页,你应该能够:
- 解释 TC、TM、RM 分别运行在哪里、保存什么状态。
- 解释
@GlobalTransactional如何开启全局事务并绑定 XID。 - 画出 XID 从入口服务传播到下游服务和数据库分支的过程。
- 说明 AT 模式第一阶段中 DataSourceProxy、SQL解析、前后镜像、undo_log、Branch Register 和本地提交的顺序。
- 解释 AT 为什么第一阶段会释放数据库行锁,却仍有全局锁语义。
- 解释普通 SELECT、
SELECT FOR UPDATE与 AT 全局读隔离的差异。 - 说明二阶段 Commit 为什么快,Rollback 为什么要校验后镜像。
- 说明脏数据、undo_log、全局锁冲突和 TC 重启怎样恢复。
- 解释 Seata XA、TCC、Saga 的执行和适用边界。
- 用 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到底是谁
flowchart TD
A["业务入口方法<br/>TM定义全局事务边界"] --> B["TC事务协调器<br/>保存全局与分支状态"]
C["订单库DataSourceProxy<br/>RM管理分支"] --> B
D["库存库DataSourceProxy<br/>RM管理分支"] --> B
E["优惠券库DataSourceProxy<br/>RM管理分支"] --> B| 角色 | 所在位置 | 主要职责 | 关键标识 |
|---|---|---|---|
| TC | Seata Server 集群 | 开启全局事务、保存状态、注册分支、管理全局锁、推动提交/回滚 | XID、branchId |
| TM | 发起全局事务的应用 | Begin、Commit、Rollback 全局事务,定义边界 | XID |
| RM | 访问事务资源的应用 | 注册分支、报告状态、申请/释放全局锁、执行分支提交或回滚 | branchId、resourceId、lockKey |
一个应用可以同时扮演 TM 和 RM:订单服务的方法开启全局事务时是 TM;它修改订单库时又作为该数据库分支的 RM。
四、全局事务生命周期
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 属于哪个全局事务。
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 会自动变成分布式事务。
先记住边界:
| 注解或机制 | 管什么 | 不管什么 |
|---|---|---|
@GlobalTransactional | TM 向 TC 开启、提交或回滚全局事务,绑定 XID | 不直接管理数据库连接的本地提交细节 |
@Transactional | 当前服务当前数据库连接的本地事务边界 | 不自动让其他服务和数据库一起回滚 |
DataSourceProxy | RM 拦截本地 SQL,生成镜像、undo_log、注册分支 | 不代理绕过数据源的手工连接或外部系统 |
| Feign/Dubbo 拦截器 | 把 XID 传到下游 | 不保证下游一定接入 RM,也不保证业务幂等 |
典型链路是:
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 等待、人工审批、文件上传都包在全局事务里。
@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 都可能不按预期增强。
public class OrderService {
public void outer() {
// 这类内部调用可能绕过代理增强
inner();
}
@Transactional
public void inner() {
// 以为开了本地事务,实际可能没有经过代理
}
}生产排查时,如果发现“加了注解但没有全局事务/本地事务”,要检查:
- 方法是否为
public且由 Spring Bean 代理调用。 - 是否同类自调用绕过代理。
- 是否异常被 catch 后吞掉,导致事务拦截器认为正常返回。
rollbackFor是否覆盖了业务自定义异常。- 数据源是否确实被 Seata 代理。
- 下游是否收到 XID。
6.3 为什么不建议把远程慢调用放进本地数据库事务
错误写法常见于:
开启本地订单事务
-> 插入订单
-> 调用库存服务
-> 调用优惠券服务
-> 调用支付预下单
-> 提交订单事务问题是本地数据库锁会在远程调用期间一直持有。下游慢、网络超时或重试时,订单库行锁、连接池和线程都会被拖住。
更好的思路是缩短每个本地事务:
flowchart TD
A["全局事务编排"] --> B["订单本地短事务"]
B --> C["库存本地短事务"]
C --> D["优惠券本地短事务"]
D --> E["全局提交或回滚"]如果流程天然很长,例如支付、物流、审批、外部医院接口采集,不要强行 AT。应优先考虑 Saga、Outbox、事务消息、状态机和补偿对账。
6.4 本地事务提交了,全局事务还能回滚吗
在 AT 模式里,一阶段本地事务确实已经提交了。全局回滚不是数据库物理回滚,而是 RM 根据 undo_log 执行一个新的补偿 SQL,把数据恢复到前镜像。
这就是为什么 AT 必须有:
- 前镜像:知道回滚目标。
- 后镜像:确认没有人绕过 Seata 改过数据。
- undo_log:保存可恢复依据。
- 全局锁:限制其他 Seata 全局事务并发改同一行。
- 幂等和对账:处理二阶段重试、冲突和人工介入。
如果你不能接受“一阶段数据短暂可见”,或者业务要求数据库层强原子提交,应评估 XA;如果业务流程很长或包含外部系统,应评估 TCC、Saga、Outbox 或事务消息。
七、AT 模式成立的前提
AT 适合 Seata 能正确代理的数据源和支持的关系数据库 SQL。它依赖:
- 应用使用 Seata DataSourceProxy 或对应自动代理。
- 业务 SQL 能被识别并生成前后镜像和回滚动作。
- 表有可定位记录的主键。
- 每个业务库存在版本匹配的
undo_log表。 - 所有需要参与全局写隔离的更新都经过 Seata 代理。
- 全局事务足够短,热点 lockKey 竞争可接受。
AT 不等于适合任意复杂 SQL、存储过程、跨库 SQL、非关系资源和外部 API。具体 SQL 支持范围必须以使用版本官方文档和集成测试为准。
八、AT 第一阶段完整执行流程
以库存 SQL 为例:
update inventory
set available = available - 2
where sku_id = 1001
and available >= 2;AT 分支的关键流程:
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 用资源和主键标识本分支修改的记录,例如概念上类似:
inventory:1001
order:ORDER-9001具体编码由 Seata 版本和资源管理实现负责,业务不应手工拼接依赖内部格式。
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、分支状态和全局锁。
flowchart TD
A["TM请求全局Commit"] --> B["TC持久化全局提交状态"]
B --> C["TC释放或推进分支全局锁状态"]
C --> D["异步通知RM清理undo_log"]
D --> E["清理失败可重试,不应撤销已提交业务"]“异步删除 undo_log”不代表可以永久不清理。大量残留会占空间、影响扫描和排查,需要监控清理失败、旧记录数量和保留策略。
十二、AT 二阶段回滚全过程
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 版本官方脚本:
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。它可能是正在进行或待恢复事务的唯一回滚依据。
十四、回滚时后镜像不一致怎么办
示例:
before: available=10
AT一阶段后 after: available=8
绕过Seata的操作把它改为7
全局回滚到来若直接恢复 available=10,会把后来合法扣减 1 覆盖掉。正确策略不是“为了自动回滚强行改回去”,而是:
- 保留 XID、branchId、表、主键和镜像证据。
- 停止无限自动覆盖。
- 查明哪条未受控写入改变了数据。
- 根据业务流水计算正确结果。
- 通过审计的补偿或人工流程修复。
- 修复所有绕过 DataSourceProxy 的写入口。
这类冲突说明隔离边界被破坏,不能只当作“undo_log异常”。
十五、AT 为什么会有额外成本
一次 UPDATE 相比普通本地事务可能增加:
- SQL 解析。
- 前镜像 SELECT。
- 后镜像 SELECT。
- undo_log 序列化和 INSERT。
- 向 TC 注册分支和全局锁网络交互。
- 二阶段清理或回滚。
批量、大范围更新会产生更大镜像和更多 lockKey。热点库存行会导致全局锁冲突。长事务会让冲突窗口扩大。因此 AT 应用于短、小、可代理、热点可控的事务,不应把报表、批量迁移或长外部调用包进全局事务。
十六、AT 最小业务 Demo 与真正发生的事情
@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());
}
}下游本地事务:
@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("库存不足");
}
}注解背后发生的是:
- 全局事务拦截器向 TC Begin。
- XID 绑定并传播。
- 订单和库存数据源代理各自建立 AT 分支。
- 每个分支保存镜像和 undo_log,并注册 lockKey。
- 业务正常时 TM 请求全局提交;异常时请求回滚。
- TC 根据持久化状态推动所有分支收敛。
业务仍要用 orderNo 唯一约束和状态机。若客户端因超时再次使用新订单号调用,全局事务框架无法判断这是同一次业务意图。
十七、Seata XA 模式全过程
XA 模式让数据库 XAResource 保持未最终提交的分支:
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 对比:
| 维度 | AT | XA |
|---|---|---|
| 一阶段业务数据 | 本地提交,可按镜像补偿 | Prepared,尚未最终提交 |
| 回滚依据 | undo_log 前后镜像 | 数据库 XA 恢复信息 |
| 锁 | 本地锁释放后由TC全局锁协调 | Prepared资源通常继续保留锁/上下文 |
| SQL要求 | 必须被AT正确解析和代理 | 依赖数据库/驱动XA支持 |
| 性能 | 一阶段快提交但有镜像与全局锁 | 多阶段和持锁成本更明显 |
XA 适合短事务和强原子提交需求,不适合把慢 HTTP、人工审批和长业务流程放在 Prepared 状态。
十八、Seata TCC 模式与事务栅栏
Seata TCC 将业务 Action 注册成分支,TC 根据全局决议调用 Confirm 或 Cancel。业务必须显式实现资源语义:
public interface InventoryTccAction {
boolean tryReserve(BusinessActionContext context,
String orderNo, Long skuId, int count);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}事务栅栏概念上按 xid + branchId + actionName 保存状态:
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 调用栈。
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 全局事务长时间不结束
- 用业务键找到入口日志中的 XID。
- 在 TC 状态存储/日志中查全局状态和最后更新时间。
- 列出 branchId、resourceId、分支状态和失败原因。
- 判断卡在 Begin、Branch Register、Global Commit、Rollback 还是二阶段重试。
- 查对应应用实例是否存活、XID 是否正确传播。
- 查数据库锁等待、连接池、undo_log 和网络。
- 分类瞬时错误、结果未知、永久业务错误和数据冲突。
22.2 Branch Register 或全局锁冲突
按 lockKey 找持有 XID,检查其全局事务是否长时间运行;再查热点主键、长远程调用和重试频率。不要只把锁重试次数调大,否则会把热点争用变成更多等待线程。
22.3 AT 回滚失败
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_logDDL、权限、表名和版本是否正确。- 业务是否在存储过程或未支持路径中修改数据。
这是高风险隔离缺口,不能只补一条 undo_log 假数据。
22.5 TC 重启后事务恢复缓慢
检查状态存储连接、恢复扫描、事务表索引、待重试数量、分支应用可达性和数据库锁。大量历史未清理事务会拖慢恢复;需要归档和清理策略,但只能删除已确认终态且满足保留要求的数据。
二十三、监控指标与告警
| 指标 | 价值 |
|---|---|
| active_global_transactions | 当前活跃全局事务 |
| begin/commit/rollback TPS | 事务流量和决议趋势 |
| commit/rollback latency P95/P99 | 协调时延和尾部问题 |
| branch_register_failures | RM注册、网络或锁问题 |
| 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:后镜像校验为什么必要
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());
}
}
}输出:
safeRollback=10
dirty data: expected after=8, current=7Demo 只是镜像校验原理。真实 Seata 会保存表、主键、字段值、序列化上下文并在本地事务中执行补偿。
二十七、常见错误与后果
| 错误 | 后果 | 正确方向 |
|---|---|---|
| 加注解后认为所有远程资源都回滚 | 未加入RM的服务仍本地提交 | 验证XID传播和资源接入 |
| AT事务中调用长外部接口 | 全局锁窗口和超时扩大 | 缩短事务,外部步骤改Saga/消息 |
| AT用于热点秒杀行 | lockKey冲突严重 | 专用库存模型、队列或TCC |
| 普通SQL绕过DataSourceProxy | TC无法建立分支,回滚冲突 | 统一数据源入口并扫描手工连接 |
| 宣称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依赖持久化状态机和补偿。生产落地必须同时设计业务幂等、超时未知、对账、监控和人工处置。
