Seata:分布式事务接入、AT 回滚与运行维护
库存扣减和账户扣款放在两个数据库里,各自的本地事务只能约束自己的写入。库存已经提交、扣款却失败时,应用需要一种跨资源的处理协议,决定怎样恢复业务结果,并在进程或网络故障之后继续处理未完成的事务。
Seata 提供这类协调能力。接入之前,应先看跨库是否必要:同一业务边界内能够用一个本地事务完成的操作,通常更简单。跨服务流程中,短事务、资源预留和异步补偿各有适用条件;选择取决于业务允许的中间状态、隔离要求和恢复成本。
全局事务由哪些对象共同完成
TC、TM、RM 和 XID
| 对象 | 所在位置 | 负责的状态 |
|---|---|---|
| TM,事务管理者 | 业务入口应用 | 开启全局事务,请求提交或回滚 |
| RM,资源管理者 | 访问资源的应用 | 注册分支,执行本地操作,接收二阶段处理请求 |
| TC,事务协调者 | Seata Server | 记录全局与分支状态,协调全局决定及重试 |
| XID | 在参与者之间传播 | 标识同一个全局事务 |
一个 Java 进程可以同时承担 TM 和 RM,也可以包含多个数据库资源。进入下游服务时,下游必须得到同一个 XID,其数据库访问也必须接入相应 RM;普通 HTTP 调用不会仅因为上游用了 Seata 就自动变成事务分支。角色和 API 见 官方 Quick Start与 API Guide。
TC 不代替数据库执行 SQL,也不保存所有业务行的副本。AT 回滚所需的前后镜像保存在业务数据库的 undo_log,由对应 RM 使用;TC 另外保存全局状态、分支信息及全局锁。两处数据需要分别持久化。
四种模式怎样选择
| 模式 | 一阶段做什么 | 二阶段与开发代价 | 常见适用条件 |
|---|---|---|---|
| AT | JDBC 代理记录镜像,提交业务数据和 undo_log | 自动清理或按镜像补偿;需满足数据库、SQL 和隔离条件 | 使用受支持关系库的短事务 |
| TCC | 业务实现 Try,预留库存、余额等资源 | 业务实现 Confirm/Cancel,处理幂等、空回滚和悬挂 | 资源预留语义清楚,可修改业务接口 |
| Saga | 各步骤提交自己的本地事务 | 按状态机执行后续或补偿;补偿也可能失败 | 长流程、外部系统参与、接受中间状态 |
| XA | 数据库资源参加 XA 协议并进入准备状态 | 由资源执行提交/回滚,持锁与连接成本较高 | 资源支持 XA,隔离要求和事务时长适合 |
AT 的自动化来自对受支持 JDBC 操作的代理;发短信、调用第三方支付、写本地文件等副作用并不会被一条 undo SQL 撤销。TCC 的 Cancel 和 Saga 的补偿则是新的业务操作,例如释放预占或发起退款,可能有时效和审批条件。模式细节分别见 AT、TCC、Saga和 XA。
AT 第一阶段为什么已经能读到变化
先核对业务表的主键、数据库类型和实际 SQL。AT 依赖识别受影响行来生成镜像,不能把任意数据库操作都交给代理:存储过程、触发器、嵌套 SQL,以及复杂多表和批量操作都有支持限制,部分能力还随数据库和版本变化。DDL 应走独立迁移流程。接入前把项目实际使用的语句逐类对照 SQL Restrictions,再验证提交和回滚。
以库存从 100 扣到 99 为例,RM 先识别 SQL 和受影响行,保存修改前镜像,执行更新,再取得修改后镜像。提交本地事务前,要完成分支注册及全局锁相关处理。库存行变化和 undo_log 在同一个本地事务中提交,然后释放数据库本地锁。
这时普通数据库连接可以读到 99,即使整个全局事务还没有完成。AT 的全局锁用于约束参与协议的写入;绕过代理的 SQL 和普通 SELECT 仍需另行考虑。需要读取隔离时,应依据产品支持的全局锁检查和业务协议设计,而非把本地数据库的 READ COMMITTED 直接等同于全局事务级别的读已提交。
全局提交后,AT 通常异步清理 undo_log。全局回滚时,RM 读取对应日志,比较当前行与后镜像,再按前镜像补偿。若期间存在绕过协议的修改,当前行可能已不等于后镜像;这时需要停下来调查脏写,不能强行覆盖别人的有效数据。具体时序见 AT 机制。
搭建两个真实分支,观察提交与回滚
环境、版本和目录
使用 Linux、Bash、Docker Engine、Compose v2、OpenSSL,预留镜像、Maven 缓存与数据库卷空间。下载 控制组件实验源码,解压后进入 control-lab。已经做过同目录 Nacos/Sentinel 实验时,保留现有 .env,不要再次生成凭据。
control-lab/
├─ compose.yaml 隔离网络与服务身份
├─ prepare.sh 首次生成随机实验凭据
├─ seata-server/Dockerfile 固定 Java 运行时和内存
└─ seata-client/
├─ init.sql 两个 schema、业务行和 undo_log
├─ pom.xml SDK、JDBC、连接池与序列化依赖
└─ src/main/
├─ java/lab/SeataApp.java
└─ resources/
├─ registry.conf
└─ file.conf实验固定 Seata Server/SDK 2.6.0、MySQL 8.4.11、Maven 3.9.12,客户端源码目标 Java 17,运行时固定 Java 25。Seata 的分发和签名入口见 Server Download。发行包、容器入口和网站上的运行说明可能面向不同分发方式,实际基线应包含容器内 java -version,不能只记录标签。
示例从 apache/seata-server:2.6.0 提取程序,放入固定 JRE 25 镜像,显式设置 -Xms128m -Xmx512m、容器内存上限 1 GiB 和运行 UID 10001。构建镜像时 root 只负责创建并授权数据目录;运行时只读根文件系统,数据写入 /data/sessionStore,日志写入临时目录。生产日志需要另接采集或持久卷。
这里是一台 TC、一个 Java 进程内的两个 RM 数据源以及一台 MySQL 的两个 schema。可以验证真实 AT 分支,不具备独立数据库主机或多个 TC 的故障隔离能力。
启动 TC 与数据库
bash prepare.sh
mkdir -p .m2
docker run --rm --user "$(id -u):$(id -g)" \
-e MAVEN_CONFIG=/tmp/.m2 \
-v "$PWD:/work" -v "$PWD/.m2:/m2" -w /work \
maven:3.9.12-eclipse-temurin-25 \
mvn -B -ntp -Dmaven.repo.local=/m2 -Duser.home=/tmp \
-pl seata-client -am package
docker compose up -d --build mysql seata
docker compose logs --tail 50 mysql seata
docker compose top先执行附件的数据库检查,成功后才进入客户端实验:
bash wait-mysql.sh检查以 lab 业务账号通过 --protocol=TCP -h127.0.0.1 认证,查询 stock、account 的 balance 和 undo_log 四张表;最多重试 90 次,每次失败后等待 2 秒,仍未就绪则返回非零并输出数据库日志。全新库应依次输出 100、1000、0、0,已有实验则显示当前值,不重新初始化。首次启动的临时 mysqld 禁用 TCP,日志中的 ready for connections 或 socket 查询成功都不足以确认初始化脚本已经完成。
同时确认 TC 日志出现 service listen port: 8091。业务与 TC 通信使用 8091;本实验不发布数据库和 TC 的宿主端口,也不启用面向不可信网络的入口。后续客户端还会校验 TM/RM 通信与实际分支结果,监听成功不能代替事务测试。
Seata 2.6 的控制台部署在 NamingServer,默认入口为 8081,不能沿用旧教程把 7091 当作本实验必然存在的页面。控制台和原生注册中心属于可选部署路径,见 Console Introduction与 NamingServer Registry。检查事务仍可从 XID、TC/RM 日志和数据库开始。
客户端通过事务组找到 TC。示例 registry.conf 选择本地文件配置,file.conf 包含:
service {
vgroupMapping.default_tx_group = "default"
default.grouplist = "seata:8091"
enableDegrade = false
disableGlobalTransaction = false
}default_tx_group 是应用事务组;default 是映射到的 TC 集群名;本地注册方式再通过 grouplist 找到地址。换成 Nacos 等注册中心时,地址发现改由对应注册实现完成。这条映射必须能被客户端加载,不能只发布在服务器自己的配置里。参数解释见 Seata Parameter Configuration。
数据源代理与本地事务
初始化 SQL 创建 stock 和 account 两个 schema,各有一行业务数据:库存 100、余额 1000。每个 schema 都有自己的 undo_log,唯一键包含 XID 与 branch_id。业务账号只用于这两个实验库;初始化 DDL 由 MySQL 入口管理身份执行。生产数据库不要用这种全新卷初始化脚本修改已有表。
客户端先初始化 TM/RM,再为两个连接池各创建一次 DataSourceProxy。更新通过代理连接执行:
TMClient.init("ops4-at", "default_tx_group");
RMClient.init("ops4-at", "default_tx_group");
DataSourceProxy stockProxy = new DataSourceProxy(stockPool);
try (Connection c = stockProxy.getConnection()) {
c.setAutoCommit(false);
try (PreparedStatement s = c.prepareStatement(
"UPDATE balance SET amount=amount-? WHERE id=1")) {
s.setInt(1, 1);
if (s.executeUpdate() != 1) {
throw new IllegalStateException("missing row");
}
c.commit();
} catch (Exception failure) {
c.rollback();
throw failure;
}
}c.commit() 提交的是一个分支本地事务,整个业务结束后还要由 TM 请求全局决定。示例直接使用 SDK,显式提供 Jackson Databind 和 JSR-310 模块供 undo 日志序列化;只引入一个 SDK 聚合包而漏掉可选序列化依赖,可能直到第一次分支提交才报错。对应实现见 JacksonUndoLogParser。
先提交一次,再故意失败一次
docker compose run --rm seata-app首次在新数据库上运行,应看到以下关键结果。XID 由 TC 生成,每次不同:
BEGIN xid=当前事务标识
PHASE1 stock=99 account=990
FINAL stock=99 account=990
PASS commitPHASE1 来自两次本地提交之后、全局提交请求之前的普通连接读取。这一步直接显示 AT 的中间状态已经可见。
随后故意在两个本地事务都提交之后抛出异常:
docker compose run --rm seata-app fail预期一度读到库存 98、余额 980;请求全局回滚并等待业务行恢复后,最终仍为 99、990:
PHASE1 stock=98 account=980
ROLLBACK requested: intentional failure after both local commits
FINAL stock=99 account=990
PASS rollback程序只有在两次本地提交确实完成、随后触发指定失败并恢复两行数据时才通过负例。数据库连接失败、SQL 错误或序列化失败会让程序非零退出,不能把“什么都没写入”当成成功验证回滚。
可用管理连接独立核对:
docker compose exec -T mysql sh -c \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot -N -e "
SELECT amount FROM stock.balance WHERE id=1;
SELECT amount FROM account.balance WHERE id=1;
SELECT COUNT(*) FROM stock.undo_log;
SELECT COUNT(*) FROM account.undo_log;"'业务值应为 99 和 990。undo_log 的清理可能稍后完成,不要用看到临时残留日志来判定业务回滚失败。重复执行成功例会继续扣减,负例则恢复到它开始前的值;若要重新得到首轮数字,应创建新的隔离实验数据库,不覆盖现有运行数据。
接入 Spring 服务并安排生产部署
全局事务入口和本地事务要配合
在已按兼容组合引入 Seata 的 Spring 工程中,全局入口可用 @GlobalTransactional,各数据库分支仍使用本地事务。以下展示方法职责,不包含完整 Web 工程配置:
@GlobalTransactional(name = "place-order", rollbackFor = Exception.class)
public OrderResult placeOrder(Command command) {
inventoryClient.reserve(command);
accountClient.debit(command);
return orderService.create(command);
}注解依赖代理调用,类内自调用等绕过代理的路径需要调整。下游失败必须沿调用链传播,不能先吞掉异常再返回“成功对象”。本地分支事务也要覆盖一个完整业务更新,避免拆成互不关联的自动提交语句。
Spring Boot、Spring Cloud Alibaba、Seata 客户端分别有自己的版本约束。不要把这个裸 SDK 工程的依赖整包复制到 Boot 4 工程:其中的框架依赖需要由适配版本和 BOM 管理。已有 Spring Cloud Alibaba 接入方法见 生态开发接入;具体框架示例从 Seata Quick Start进入后选择与项目一致的分支。
XID 的传播、线程与重试
上游获取 XID 后,由所用 RPC/HTTP 适配层传到下游;下游在处理期间绑定 RootContext,结束时解除绑定。正确的过滤器还要保存并恢复原有上下文,避免线程池复用后把上一请求的事务带入下一请求。先查框架是否已经安装适配器,重复绑定和重复数据源代理同样会出问题。
线程池、异步任务与消息消费不自动延续调用线程中的 XID。事务还没完成时发出的异步任务可能晚于全局超时才开始;应明确等待、超时和补偿协议,或改成可靠事件/状态机,不把 ThreadLocal 拷贝当作长流程事务设计。
请求超时后,业务是否已完成可能未知。用订单号、请求幂等键和 XID 查询结果,再决定补偿或重试。重复发起一个新全局事务并不会自动识别前一个业务请求。相关上下文 API 见 API Guide。
TC 发现与持久化分别配置
生产可以使用 Nacos 发现 TC,并用数据库存储 TC 状态。两项选择互相独立:注册中心解决“连到谁”,事务存储解决“故障后还记得什么”。配置中心又是第三项职责,用于分发参数。
Nacos 注册要对齐 serverAddr、Namespace ID、Group、注册服务名和 cluster;客户端的事务组映射要指向同一个 cluster。还要检查 TC 注册的 IP 和 8091 从业务网络是否可达。精确配置例见 Nacos Registry Center,Nacos 自身的鉴权和生命周期见 Nacos 运维。
DB 存储的 TC 通常使用独立数据库。按对应发行版本导入官方 TC schema,再设置 store.mode=db、数据源实现、驱动、URL 和专用账号。主要表包括 global_table、branch_table、lock_table,以及管理任务协调使用的 distributed_lock;这些表不替代每个业务库里的 undo_log。完整参数和分离 lock/session 存储的限制见 配置表。
多台 TC 指向同一套符合要求的存储,才能共同处理相应事务;复制一份本地 file 目录给另一台机器,不会得到可安全接管的共享事务状态。使用 Raft 存储则要按该模式部署成员、日志与多数派,不能再机械套用 DB 模式的共享表假设。
权限、容量和滚动维护
业务 RM 需要业务表和 undo_log 所需权限;TC 账号访问自己的事务状态表;注册配置账号按资源授权。TC 和管理页面只开放在可信网络,并依据运行版本配置客户端鉴权及传输保护。凭据轮换要覆盖全部参与方,先观察新连接,再撤销旧凭据。
容量评估要计算活跃全局事务数、分支数、热点锁竞争、undo_log 字节量和二阶段积压。长事务会扩大等待和恢复范围;将全局超时调大,往往只是把问题保留得更久。数据库慢 SQL、HTTP 超时和全局超时需要形成合理顺序,使失败能在上游等待窗口内传回。
滚动维护前,确认目标版本支持的协议、SQL 解析和 undo 序列化兼容。先在隔离环境验证“旧版本产生的一阶段状态能被新版本恢复”,再更新少量客户端或 TC。不要在未完成事务仍依赖旧序列化格式时随意删掉旧解析器。升级入口见 Version Upgrade Guide。
从 XID 定位失败并恢复事务
先确认失败发生在哪一阶段
| 现象 | 应关联的信息 | 处理与复核 |
|---|---|---|
| 无法开启全局事务 | 事务组映射、TC 地址、鉴权、客户端连接日志 | 修复连接后先验证 begin;不要关闭全局事务开关继续当作成功 |
| 业务写入没有分支 | XID、实际 DataSource 类型、本地事务调用路径 | 补齐上下文和单层代理,再用失败例验证两个库恢复 |
| 提交时报 UndoLogParser/类缺失 | 运行时 classpath、序列化器、缺失类 | 补齐对应依赖并重建,不能仅凭 Maven 编译成功判定可用 |
| 全局锁竞争或本地死锁 | XID、branchId、resourceId、表与主键、数据库锁等待 | 缩小事务、消除热点,避免参与与非参与写入混杂 |
| 二阶段不断重试 | TC 状态、RM 是否在线、数据库和 undo_log | 恢复依赖,再看同一个 XID 的分支是否完成 |
| 回滚发现脏数据 | 前镜像、后镜像、当前行与其他业务写入 | 暂停冲突写入,调查来源,按业务决定补偿方式 |
日志中至少关联 XID、branchId 和数据库 resourceId。不要只搜索“rollback”然后重启所有应用:一台仍持有回滚能力的 RM 被停掉,可能让恢复进一步延迟。TC 重试参数影响处理周期和停止条件,具体语义见 配置表。
恢复事务需要两侧材料
TC 存储、业务数据与 undo_log 要一起纳入备份策略。只恢复 TC 旧状态,却保留已经变化的业务行,可能触发重复协调或镜像冲突;只恢复业务库,TC 也可能已经忘记相应事务。生产恢复应先隔离写入和旧参与者,选定一致的恢复方案,确认待处理事务集合,再逐步恢复 RM 连接。
故障处理保留原日志和相关行的只读副本。控制台的手动提交、回滚、删除记录或解锁属于高风险业务操作,要先知道该 XID 在每个参与方的结果。清空 lock_table、删除 undo_log 或把全局状态改成成功,都可能让表面告警消失而破坏可恢复性。
对无法自动补偿的外部副作用,应转入有业务凭据的对账与人工处理流程。恢复判断看最终订单、库存、资金及外部结果,而不只看 TC 记录是否已经清空。
实验结束先确认两次结果已输出,再停止应用、TC 和 MySQL。保留命名卷可继续检查文件和业务行;不要在仍有未完成事务时直接删除卷。该实验验证的是同主机两个真实 AT 分支,生产多节点接管、数据库灾难恢复和人工事务干预还需要专门演练。
权威资料与规范地址
事务模型与客户端
| 查阅内容 | 官方地址 |
|---|---|
| 初次接入 | Quick Start |
| TM、RM 与上下文 API | API Guide |
| AT 两阶段 | AT Mode |
| SQL 支持限制 | SQL Restrictions |
| 业务预留模式 | TCC Mode |
| 长流程补偿 | Saga Mode |
| XA 协议 | XA Mode |
| undo 序列化实现 | JacksonUndoLogParser |
部署、发现与升级
| 查阅内容 | 官方地址 |
|---|---|
| 分发与签名 | Server Download |
| 控制台位置与职责 | Console Introduction |
| 原生注册中心 | NamingServer Registry |
| 配置字段 | Parameter Configuration |
| Nacos 发现 | Nacos Registry |
| 升级 | Version Upgrade Guide |
