分布式事务
项目默认不开启分布式事务。一个业务操作需要同时修改多个数据库,并且要求它们要么都成功、要么都回滚时,可以基于 Apache Seata (opens new window) 的 AT 模式按需接入,实际使用的依赖版本见《3.3 添加依赖》小节。
接入方式是在 《多数据源(读写分离)》 的基础上,由 Dynamic Datasource 为指定的数据源包装 Seata 代理:
master主库、slave从库保持不变,不参与全局事务。工作流、商城、支付等已有业务的 SQL,不会因为接入 Seata 而自动变成分布式事务。- 只有添加了
@GlobalTransactional注解的入口方法,以及它调用的、开启了 Seata 代理的数据源(示例是seata_order、seata_account),才组成一个全局事务。
友情提示:
分布式事务会带来额外的 Seata Server 部署、全局锁、回滚日志等成本。单库能完成的业务,优先使用本地事务;多个数据源的场景,也可以先评估 《多数据源(读写分离)》 的 @DSTransactional 或补偿逻辑是否满足需求。
# 1. 方案说明
# 1.1 整体结构
DemoSeataTradeService#placeOrder @GlobalTransactional(TM,全局事务的发起方)
├── DemoSeataOrderService#createOrder @DS("seata_order") + @Transactional(分支一)
│ └── seata_order 数据源(Druid 连接池 + Seata DataSourceProxy)
│ └── yudao_seata_order 库:demo_seata_order 表 + undo_log 表
└── DemoSeataAccountService#deductBalance @DS("seata_account") + @Transactional(分支二)
└── seata_account 数据源(Druid 连接池 + Seata DataSourceProxy)
└── yudao_seata_account 库:demo_seata_account 表 + undo_log 表
Seata Server(TC,事务协调者):记录全局事务及其分支,决定二阶段提交或回滚
# 1.2 AT 模式的执行过程
AT 模式对业务代码没有侵入,业务代码仍然是普通的 MyBatis Plus 读写:
① 一阶段:每个分支执行业务 SQL 时,Seata 的 DataSourceProxy 先记录修改前、后的数据镜像,与业务数据在同一个本地事务中写入 undo_log 表,然后立即提交本地事务,并向 Seata Server 注册分支。
② 二阶段提交:全局事务的入口方法正常结束,Seata Server 通知各分支异步删除 undo_log 即可,业务数据在一阶段已经提交。
③ 二阶段回滚:入口方法抛出异常,Seata Server 通知各分支根据 undo_log 生成反向 SQL,将数据恢复到修改前的镜像。
所以,一阶段提交后、二阶段完成前,其它不在全局事务中的查询可以读到“中间状态”的数据,参见《4.3 隔离性》小节。
# 1.3 示例
项目提供了两个独立的演示库,不会修改已有的业务表:
| 数据源 | 库 | 表 | 说明 |
|---|---|---|---|
seata_order | yudao_seata_order | demo_seata_order | 订单,主键 id 由调用方传入,作为幂等主键 |
seata_account | yudao_seata_account | demo_seata_account | 账户,初始化租户 1、用户 1 的账户,余额 100 分 |
两张表都继承项目的 TenantBaseDO,带有 create_time、update_time(LocalDateTime)和 tenant_id 字段,可以在回滚后核对时间、租户字段是否一起恢复。
示例 DemoSeataTradeService#placeOrder 的执行顺序是:先在订单库创建订单,再在账户库条件扣减余额(balance >= 金额 时才更新):
| 场景 | 结果 |
|---|---|
| 余额足够 | 两个分支都提交:订单存在,余额减少 |
| 余额不足 | 分支二抛出异常,分支一的订单根据 undo_log 删除:订单不存在,余额不变 |
| 两个分支都成功,外层的全局事务随后失败 | 两个分支都根据 undo_log 回滚:订单不存在,余额恢复 |
| 重复的订单编号 | 分支一主键冲突、抛出异常,不会重复扣减余额 |
为什么不直接使用商城、支付的业务做示例?
商城下单、支付等真实流程,涉及库存、优惠券、积分、钱包等多张表,以及消息队列、定时任务、第三方回调等不受数据库事务控制的操作,需要按业务逐个评估回滚方式。所以,示例使用独立的演示库,接入 Seata 不等于商城、支付等业务自动启用了分布式事务。
# 2. 获取集成材料
集成材料在项目的 script/seata/ (opens new window) 目录。目前只有 master-jdk17 分支提供这些材料,master、master-jdk25 分支请先从 master-jdk17 分支获取,同步到对应分支后,也可以直接从当前分支获取。材料适用于下面三个分支,接入步骤相同,兼容差异如下:
| 分支 | JDK | Spring Boot | 差异 |
|---|---|---|---|
master | 8 | 2.7.18 | 无,Jackson 2 已由 Spring Boot 提供 |
master-jdk17 | 17 | 3.5.15 | 无,Jackson 2 已由 Spring Boot 提供 |
master-jdk25 | 25 | 4.1.2 | Spring Boot 4 默认使用 Jackson 3,必须添加 dependencies.xml 中的 Jackson 2 依赖,见《3.3 添加依赖》小节 |
目录中的文件及用途如下:
| 文件 | 说明 | 目标位置 |
|---|---|---|
sql/mysql/demo_seata.sql | 创建 2 个演示库,各一张示例表 + undo_log 表 | 在 MySQL 中执行 |
dependencies.xml | Maven 依赖片段 | yudao-module-system/pom.xml |
seata.yaml | 数据源、Seata 客户端的配置片段 | yudao-server 的 application-local.yaml 等配置文件 |
src/main/java/.../system/seatademo/ | 订单 DO、Mapper、Service,全局事务入口 DemoSeataTradeService | yudao-module-system/src/main/java/ 的同名包 |
src/main/java/.../infra/seatademo/ | 账户 DO、Mapper、Service | yudao-module-infra/src/main/java/ 的同名包 |
订单放在 yudao-module-system、账户放在 yudao-module-infra,是因为前者已经依赖后者,可以直接调用账户 Service。
# 3. 接入步骤
# 3.1 启动 Seata Server
Seata Server(TC)是独立的进程,与项目使用的 JDK 版本无关。本机演示时,使用官方 Docker 镜像启动即可:
docker run -d --name yudao-seata-server \
-p 127.0.0.1:18091:18091 \
-e SEATA_PORT=18091 \
-e JVM_XMS=256m \
-e JVM_XMX=512m \
apache/seata-server:2.6.0
-p 127.0.0.1:18091:18091:只在本机的127.0.0.1上映射端口,不对局域网、公网开放;后端项目与 Docker 运行在同一台机器上,通过grouplist直连127.0.0.1:18091。SEATA_PORT:Seata Server 的服务端口,必须与《3.4 添加配置》小节中seata.service.grouplist.default的端口一致。JVM_XMS、JVM_XMX:限制 JVM 内存,本机演示足够。
这里没有设置 SEATA_IP:Seata Server 不接受 127.0.0.1 这类回环地址作为自己的地址,会改用容器内的 IP,日志、XID 中看到 172.17.* 之类的地址属于正常现象,不影响本机演示。后端项目与 Seata Server 不在同一台机器时,需要使用后端项目能够访问到的真实地址、端口,参见下面的生产部署说明。
执行 docker logs -f yudao-seata-server 命令,确认日志中没有异常、服务已经启动在 18091 端口。
本机演示 ≠ 生产部署
上面的命令使用镜像默认的 file 存储模式,全局事务的状态保存在容器内的文件中,容器删除或重建后会丢失,也只有单个节点。生产环境需要:
- 将存储模式改为
db(需要初始化 Seata Server 自己的表)或raft等可持久化的方式,并部署多个节点保证高可用。 - 将客户端的
seata.registry、seata.config改为 Nacos 等注册中心、配置中心,而不是file直连。 - Seata Server 注册、声明的地址,必须是业务服务能够访问到的真实地址、端口,不能依赖
127.0.0.1等回环地址。 - 为 Seata Server 设置访问控制,只允许业务服务所在的内网访问。
具体参见 Seata 官方的 《新人文档》 (opens new window)、《Docker 部署》 (opens new window)、《高可用部署》 (opens new window) 文档。
# 3.2 初始化数据库
在 MySQL 中执行 sql/mysql/demo_seata.sql 脚本,创建 yudao_seata_order、yudao_seata_account 两个库,以及:
- 业务表:
demo_seata_order、demo_seata_account,并初始化租户 1、用户 1 的账户,余额 100 分。 undo_log表:AT 模式的回滚日志,每个参与全局事务的库都要有一张,与业务表在同一个库中。表结构来自 Seata 官方的script/client/at/db/mysql.sql(opens new window),Seata Server 不需要这张表。
脚本使用「库名.表名」的写法、CREATE ... IF NOT EXISTS 和 INSERT IGNORE,可以重复执行,不会删除已有的库、表、数据,也不会修改 master 主库。
# 3.3 添加依赖
将 dependencies.xml 中的依赖,复制到 yudao-module-system 的 pom.xml 中:
- 全局事务的入口
DemoSeataTradeService在yudao-module-system模块中,直接引用了 Seata 的@GlobalTransactional注解,所以该模块编译时就需要 Seata 依赖。只加在yudao-server/pom.xml中时,yudao-module-system会编译失败。 yudao-server已经依赖yudao-module-system,会通过依赖传递获得 Seata,运行时不需要重复添加。- 账户示例所在的
yudao-module-infra不直接引用 Seata 的类,不需要添加。自己的业务中,哪个模块使用了 Seata 的注解、类,就添加到哪个模块。
<!-- yudao-module-system/pom.xml 的 <dependencies> 中 -->
<dependency>
<groupId>org.apache.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.6.0</version>
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
① Seata 使用 org.apache.seata 的坐标,并显式指定 2.6.0 版本。它的 seata-all 自带 io.seata 包的兼容类,Dynamic Datasource 4.6.0 包装数据源时使用的 io.seata.rm.datasource.DataSourceProxy 就由它提供,不要再引入 io.seata:* 的旧坐标。
② 排除 Seata 传递的 druid 1.2.25,避免它把项目原有的 Druid 降级。排除后,Seata 解析 SQL 时继续使用项目 Druid starter 提供的版本。
③ Seata 默认使用 Jackson 2 序列化 undo_log 中的数据镜像。master、master-jdk17 分支的 Spring Boot 已经提供,声明后版本不变;master-jdk25 分支的 Spring Boot 4 默认使用 Jackson 3,必须添加这两个依赖,版本由 Spring Boot BOM 管理。它们只给 Seata 内部使用,不会替换项目接口、Redis 等使用的 JSON 序列化。
# 3.4 添加配置
将 seata.yaml 中的配置,合并到 yudao-server 的 application-local.yaml 配置文件中:
spring:
datasource:
dynamic:
seata: true # 开启 Dynamic Datasource 的 Seata 支持,由它为每个数据源包装 Seata 的 DataSourceProxy
seata-mode: AT # 使用 AT 模式
datasource:
master:
seata: false # 只追加这一行:master 主库不参与 AT,不被 Seata 代理
slave:
seata: false # 只追加这一行:slave 从库不参与 AT,不被 Seata 代理;没有配置 slave 时,删除整个 slave 段
seata_order: # 分布式事务示例的订单库,只有 @DS("seata_order") 的 Mapper、Service 使用
url: ${YUDAO_SEATA_ORDER_URL:jdbc:mysql://127.0.0.1:3306/yudao_seata_order?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true}
username: ${YUDAO_SEATA_DB_USERNAME:root}
password: ${YUDAO_SEATA_DB_PASSWORD:}
seata: true # 参与 AT:被 Seata 代理,本地事务提交时注册分支、写入 undo_log
seata_account: # 分布式事务示例的账户库,只有 @DS("seata_account") 的 Mapper、Service 使用
url: ${YUDAO_SEATA_ACCOUNT_URL:jdbc:mysql://127.0.0.1:3306/yudao_seata_account?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true}
username: ${YUDAO_SEATA_DB_USERNAME:root}
password: ${YUDAO_SEATA_DB_PASSWORD:}
seata: true # 参与 AT:被 Seata 代理,本地事务提交时注册分支、写入 undo_log
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: yudao_seata_demo_group # 事务分组,与下面的 vgroup-mapping 对应
enable-auto-data-source-proxy: false # 必须关闭:数据源已经由 Dynamic Datasource 代理,避免双重代理
client:
http:
interceptor-enabled: false # 单体项目不需要从 HTTP 请求头 TX_XID 接收 XID;关闭后,外部请求不能通过该请求头加入全局事务
service:
vgroup-mapping:
yudao_seata_demo_group: default # 事务分组 -> Seata Server 集群名
grouplist:
default: ${YUDAO_SEATA_SERVER_ADDR:127.0.0.1:18091} # Seata Server 地址,端口与启动 Seata Server 时的 SEATA_PORT 一致
registry:
type: file # 直连上面的 grouplist,仅用于本机演示
config:
type: file
① 合并,而不是替换:master、slave 原有的 url、username、password 等配置保持不变,只追加 seata: false 一行。Dynamic Datasource 的 seata 默认对每个数据源都是开启的,不追加时,主库、从库也会被 Seata 代理、向 Seata Server 注册。seata: false 只追加到当前 profile 已经存在的数据源上:没有配置 slave 时,不要复制 slave 段,否则会新建一个没有 url 的 slave 数据源,导致启动失败。
② seata.enable-auto-data-source-proxy 必须为 false。否则 Seata 会再代理一次 Dynamic Datasource 的路由数据源,形成双重代理。
③ 数据库的账号密码通过环境变量传入,不要把真实密码写入配置文件、提交到 Git:
| 环境变量 | 说明 | 默认值 |
|---|---|---|
YUDAO_SEATA_ORDER_URL | 订单库的 JDBC URL | jdbc:mysql://127.0.0.1:3306/yudao_seata_order?... |
YUDAO_SEATA_ACCOUNT_URL | 账户库的 JDBC URL | jdbc:mysql://127.0.0.1:3306/yudao_seata_account?... |
YUDAO_SEATA_DB_USERNAME | 两个演示库的账号 | root |
YUDAO_SEATA_DB_PASSWORD | 两个演示库的密码 | 空,按实际设置 |
YUDAO_SEATA_SERVER_ADDR | Seata Server 的地址 | 127.0.0.1:18091 |
本地开发时,在 IDEA 启动 YudaoServerApplication 的 Run Configuration 中,设置 Environment variables 即可。
# 3.5 复制示例代码
① 将 src/main/java 下的 cn.iocoder.yudao.module.system.seatademo 包,复制到 yudao-module-system/src/main/java 的同名目录:
DemoSeataOrderDO、DemoSeataOrderMapper:订单表,Mapper 上添加@DS("seata_order")注解。DemoSeataOrderService:类上添加@DS("seata_order")注解,createOrder方法添加@Transactional(rollbackFor = Exception.class),是全局事务的分支一。DemoSeataTradeService:placeOrder方法添加@GlobalTransactional注解,是全局事务的入口。
② 将 cn.iocoder.yudao.module.infra.seatademo 包,复制到 yudao-module-infra/src/main/java 的同名目录:
DemoSeataAccountDO、DemoSeataAccountMapper:账户表,Mapper 上添加@DS("seata_account")注解,updateBalanceDecr方法按balance >= 金额条件扣减。DemoSeataAccountService:类上添加@DS("seata_account")注解,deductBalance方法添加@Transactional(rollbackFor = Exception.class),是全局事务的分支二;余额不足时抛出业务异常。
全局事务的入口 placeOrder:
@GlobalTransactional(name = "demo-seata-place-order", rollbackFor = Exception.class)
public void placeOrder(Long orderId, Long userId, Integer amount) {
// 1. 创建订单:分支一,本地事务提交后,订单已经写入 seata_order 库,同时记录 undo_log
demoSeataOrderService.createOrder(orderId, userId, amount);
// 2. 扣减余额:分支二;余额不足时抛出异常,Seata 根据 undo_log 删除分支一已经提交的订单
demoSeataAccountService.deductBalance(userId, amount);
}
示例代码只使用 Java 8 语法和构造方法注入,不引用 javax、jakarta 包,三个分支均无需修改。
# 3.6 启动验证
① 在 cn.iocoder.yudao.module.system.seatademo 包下,新增两个临时的类。
DemoSeataVerifyService:模拟“下单成功后,外层的后续步骤失败”。它必须是单独的 @Service 类,同一个类内部的 this.xxx() 调用不会经过 @GlobalTransactional 的拦截:
package cn.iocoder.yudao.module.system.seatademo;
import lombok.RequiredArgsConstructor;
import org.apache.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
/**
* 分布式事务示例:验证外层全局事务失败时的回滚,验证完成后删除
*
* @author 芋道源码
*/
@Service
@RequiredArgsConstructor
public class DemoSeataVerifyService {
private final DemoSeataTradeService demoSeataTradeService;
@GlobalTransactional(name = "demo-seata-verify", rollbackFor = Exception.class)
public void placeOrderThenFail(Long orderId, Long userId, Integer amount) {
demoSeataTradeService.placeOrder(orderId, userId, amount); // 加入外层的全局事务,两个分支都提交成功
throw new IllegalStateException("模拟下单之后的步骤失败");
}
}
DemoSeataVerifyRunner:在项目启动完成后,在租户编号为 1 的上下文中,依次执行三个场景,并核对订单是否存在、余额是否符合预期。网络、数据库暂时不可用等普通的回滚失败,Seata Server 会自动重试,所以核对时有限次轮询,等待数据达到预期,不以执行完成后立即读取的结果为准:
package cn.iocoder.yudao.module.system.seatademo;
import cn.hutool.core.thread.ThreadUtil;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.infra.seatademo.DemoSeataAccountService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
/**
* 分布式事务示例:启动后验证提交、回滚,验证完成后删除
*
* @author 芋道源码
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class DemoSeataVerifyRunner implements ApplicationRunner {
private static final Long USER_ID = 1L;
private final DemoSeataTradeService demoSeataTradeService;
private final DemoSeataVerifyService demoSeataVerifyService;
private final DemoSeataOrderService demoSeataOrderService;
private final DemoSeataAccountService demoSeataAccountService;
@Override
public void run(ApplicationArguments args) {
TenantUtils.execute(1L, () -> {
long orderId = System.currentTimeMillis(); // 每次启动使用新的订单编号
// 场景一:余额足够,两个分支都提交:订单存在,余额减少 10
verify("成功", orderId, () -> demoSeataTradeService.placeOrder(orderId, USER_ID, 10), true, 10);
// 场景二:余额不足,分支二失败,分支一根据 undo_log 回滚:订单不存在,余额不变
verify("余额不足", orderId + 1, () -> demoSeataTradeService.placeOrder(orderId + 1, USER_ID, 100000000), false, 100000000);
// 场景三:两个分支都已提交,外层的全局事务失败,两个分支都根据 undo_log 回滚:订单不存在,余额不变
verify("外层失败", orderId + 2, () -> demoSeataVerifyService.placeOrderThenFail(orderId + 2, USER_ID, 10), false, 10);
});
}
private void verify(String scene, Long orderId, Runnable action, boolean expectCommit, int amount) {
int balanceBefore = demoSeataAccountService.getAccount(USER_ID).getBalance();
boolean committed;
try {
action.run();
committed = true;
log.info("[{}] 全局事务提交", scene);
} catch (Exception ex) {
committed = false;
log.info("[{}] 全局事务回滚:{}", scene, ex.getMessage());
}
// 有限次轮询,等待订单、余额达到预期:最多 10 次,每次间隔 500 毫秒
int expectBalance = expectCommit ? balanceBefore - amount : balanceBefore;
boolean orderExists = false;
int balance = balanceBefore;
for (int i = 0; i < 10; i++) {
orderExists = demoSeataOrderService.getOrder(orderId) != null;
balance = demoSeataAccountService.getAccount(USER_ID).getBalance();
if (orderExists == expectCommit && balance == expectBalance) {
break;
}
ThreadUtil.sleep(500);
}
if (committed == expectCommit && orderExists == expectCommit && balance == expectBalance) {
log.info("[{}] 符合预期:订单是否存在:{},余额:{} -> {}", scene, orderExists, balanceBefore, balance);
} else {
log.error("[{}] 不符合预期:是否提交:{}(预期 {}),订单是否存在:{}(预期 {}),余额:{}(预期 {})",
scene, committed, expectCommit, orderExists, expectCommit, balance, expectBalance);
}
}
}
② 依次启动 Seata Server、设置好《3.4 添加配置》小节的环境变量,再启动后端项目。三个场景都应该输出「符合预期」,出现「不符合预期」时参见《5. 常见问题》排查。首次执行时(余额从 100 开始),日志示例如下,仅供参考,时间、线程等前缀已省略:
[成功] 全局事务提交
[成功] 符合预期:订单是否存在:true,余额:100 -> 90
[余额不足] 全局事务回滚:用户(1) 的账户不存在或余额不足
[余额不足] 符合预期:订单是否存在:false,余额:90 -> 90
[外层失败] 全局事务回滚:模拟下单之后的步骤失败
[外层失败] 符合预期:订单是否存在:false,余额:90 -> 90
启动日志中,一般还可以看到 Seata 的 TM、RM 连接 Seata Server 的日志,具体内容以实际使用的 Seata 版本为准。
③ 直接连接 MySQL,核对两个库的数据:最新的一条订单是「成功」场景的订单,另外两个订单编号不存在;首次执行后账户余额为 90。undo_log 在二阶段完成后删除,删除是异步的,稍等片刻后再核对,应为 0 条;仍有记录时,查看 Seata Server 的日志,确认是否有全局事务在重试提交、回滚,或者出现了《5.5》小节中不可重试的脏数据回滚失败:
SELECT id, user_id, amount, tenant_id, create_time FROM yudao_seata_order.demo_seata_order ORDER BY id DESC LIMIT 3;
SELECT id, user_id, balance, tenant_id, update_time FROM yudao_seata_account.demo_seata_account;
SELECT 'order' AS db, COUNT(*) FROM yudao_seata_order.undo_log
UNION ALL SELECT 'account', COUNT(*) FROM yudao_seata_account.undo_log;
验证完成后,记得删除 DemoSeataVerifyService、DemoSeataVerifyRunner 两个类,否则每次启动都会扣减演示账户的余额。需要重新演示时,只修改演示库的数据即可,例如说 UPDATE yudao_seata_account.demo_seata_account SET balance = 100 WHERE id = 1;。
# 4. 使用说明
# 4.1 全局事务的写法
① @GlobalTransactional 只添加在全局事务的入口方法上,包名是 org.apache.seata.spring.annotation。入口方法所在的类,需要通过 @Service 等组件扫描注册。Seata 2.6 不会增强 @Bean 方法定义的 Bean,此时 @GlobalTransactional 不生效、也不会报错。
② 入口方法不要再添加 @Transactional。本地事务会在开始时固定一个数据源的连接,后续的 @DS 切换不会生效,参见 《多数据源(读写分离)》 的事务说明。
③ 每个分支在单独的 Service 上添加 @DS + @Transactional(rollbackFor = Exception.class),先切换数据源、再开启本地事务。同一个分支内的多条 SQL,在一个本地事务中提交。
④ 分支、入口方法中的异常要抛出,不要 try catch 后吞掉,否则 Seata 认为执行成功,会提交全局事务。
⑤ 和 @Transactional 一样,通过 this.xxx() 自调用、或者在其它线程中执行时,注解不会被拦截。
# 4.2 不在全局事务中的范围
master、slave等配置了seata: false的数据源:即使在@GlobalTransactional方法中执行,也是普通的本地 SQL,全局事务回滚时不会恢复。需要参与时,在该数据源中创建undo_log表,并去掉seata: false。- 异步线程:XID 保存在当前线程中,
@Async、线程池、CompletableFuture等异步执行的 SQL,不会自动加入全局事务。 @DSTransactional:它是 Dynamic Datasource 自己的多数据源本地事务,与 Seata 的全局事务是两套机制,不要混用在同一个调用链路中。- 《分库分表》 的
sharding数据源:不要为 ShardingSphere 的逻辑连接开启 Seata 代理,本方案没有验证两者的组合。 - 消息队列、Redis、文件、第三方接口等非数据库的操作:不会回滚,需要自行补偿,或者在全局事务提交后再执行。
# 4.3 隔离性
① 写隔离:一阶段提交前,Seata 会获取被修改记录的全局锁。其它全局事务修改同一条记录时,会等待锁释放,等待超时后失败、回滚。所以,热点数据(例如说同一个账户)的并发会下降。
② 读隔离:默认是读未提交。不在全局事务中的查询,可以读到其它全局事务一阶段已提交、但最终会回滚的数据。需要读已提交时,查询语句使用 SELECT ... FOR UPDATE,并在方法上添加 @GlobalTransactional 或 @GlobalLock 注解。
③ 回滚时的数据校验:回滚前,Seata 会比较当前数据与一阶段记录的修改后镜像。回滚失败分为两种:
- 网络中断、数据库暂时不可用等普通失败:Seata Server 会自动重试回滚。
- 其它不在全局事务中的 SQL 修改了这条记录(脏数据):该分支的回滚失败且不可重试。Seata Server 停止自动重试,全局事务以回滚失败结束,等待也不会自动恢复;对应业务库的
undo_log会保留该分支的记录,需要人工核对、修复数据后再清理,参见《5.5 回滚失败,日志出现Has dirty records when undo》小节。
所以,被全局事务修改的表,其它写入也要通过全局事务或 @GlobalLock。
详细说明,参见 Seata 官方的 《AT 模式》 (opens new window) 文档。
# 4.4 接入自己的业务
在示例的基础上,接入自己的业务时:
① 在每个参与全局事务的库中创建 undo_log 表,业务表必须有主键。
② 在 spring.datasource.dynamic.datasource 中,为这些库配置数据源,seata 保持默认的 true;其它数据源配置 seata: false。
③ 按《4.1 全局事务的写法》小节编写入口、分支,分支的 Mapper、Service 上添加对应的 @DS 注解。
④ 调用方需要重试时,像示例的订单编号一样,使用业务唯一键保证幂等。
⑤ @GlobalTransactional 的超时时间默认是 60 秒,可以通过 timeoutMills 属性调整。全局事务越长,全局锁持有越久,尽量只包含必须一致的数据库操作。
除 MySQL 外的数据库,需要使用 Seata 官方对应数据库的 undo_log 建表 SQL,并自行验证;本文只验证了 MySQL。
# 5. 常见问题
# 5.1 报错 NoClassDefFoundError: com/fasterxml/jackson/...
例如说 com/fasterxml/jackson/core/JsonProcessingException、com/fasterxml/jackson/datatype/jsr310/JavaTimeModule,通常出现在 master-jdk25 分支第一次提交分支事务时。缺少 Seata 序列化 undo_log 使用的 Jackson 2 依赖,按《3.3 添加依赖》小节,同时添加 jackson-databind、jackson-datatype-jsr310 两个依赖。
# 5.2 报错 can not get cluster name in registry config 或 no available service found in cluster
客户端没有找到、或者连接不上 Seata Server:
seata.tx-service-group与seata.service.vgroup-mapping的 key 不一致。- Seata Server 没有启动,或者
grouplist的端口与启动时的SEATA_PORT不一致。 - Docker 端口只绑定了
127.0.0.1,后端项目却运行在另一台机器、另一个容器中。
# 5.3 抛出了异常,数据却没有回滚
按顺序排查:
@GlobalTransactional是否生效:入口类是否通过组件扫描注册,是否通过this.xxx()自调用,是否引入了错误包名的注解。- 入口方法是否添加了
@Transactional,导致@DS切换失效、所有 SQL 都在一个数据源中执行。 - 异常是否被
try catch吞掉。 - 没有回滚的数据所在的数据源,是否配置了
seata: false,或者是在异步线程中执行的。 - 该数据源是否被 Seata 代理:可以查看启动、执行时 Seata RM 相关的日志,具体内容以实际使用的 Seata 版本为准。
# 5.4 启动日志出现 Seata auto-agent detected 警告
seata.enable-auto-data-source-proxy 没有设置为 false,Dynamic Datasource 的路由数据源又被 Seata 代理了一次,按《3.4 添加配置》小节关闭即可。
注意,Dynamic Datasource 4.6.0 的这个检查依赖 Seata 旧版的类名,在 Seata 2.6 中可能不会打印警告,所以必须显式配置 false,不能依赖该警告判断。
# 5.5 回滚失败,日志出现 Has dirty records when undo
一阶段提交后、二阶段回滚前,这条记录被不在全局事务中的 SQL 修改了,Seata 无法安全地恢复数据,参见《4.3 隔离性》小节。
这是不可重试的回滚失败,与网络、数据库暂时不可用等可以自动重试的失败不同:Seata Server 会停止自动重试,全局事务以回滚失败结束,等待不会自动恢复,Seata Server 中也不一定还保留可以操作的全局事务。处理方式:
- 在报错分支所在的业务库中,根据日志中的
xid、branchId查询undo_log,该分支的记录会保留,其中有一阶段的修改前、修改后镜像。 - 对照镜像与当前数据,人工核对、修复业务数据。
- 确认修复完成后,再删除这条全局事务对应的
undo_log记录。
# 5.6 Druid 的版本变成了 1.2.25
seata-spring-boot-starter 没有排除 com.alibaba:druid,参见《3.3 添加依赖》小节。可以通过 mvn dependency:tree -Dincludes=com.alibaba:druid 查看实际使用的版本。