1. 为什么Seata不是“装上就能用”的中间件——从一次服务调用失败说起
我第一次在某跨平台系统里集成Seata,是接到一个紧急需求:订单创建后要同步扣减库存和更新用户积分,三个服务必须全部成功或全部回滚。当时想得很简单——“不就是分布式事务嘛,Seata官网有QuickStart,照着跑个Demo,改几行配置,上线就完事”。结果本地跑通后一上测试环境,订单服务刚发起全局事务,库存服务就报io.seata.core.exception.TransactionException: No available service,日志里连Failed to register branch都刷屏。整整两天卡在这儿,翻遍GitHub Issues、Stack Overflow、中文社区帖子,发现90%的提问者都栽在同一类问题上:不是Seata Server没起来,就是客户端连不上注册中心,再就是AT模式下SQL解析失败却只报一句“unknown error”。
这根本不是Seata本身的问题,而是我们把分布式事务中间件当成了单机数据库驱动来用。Seata不是“安装包”,它是一套需要三方协同的运行时契约:Server端要稳定提供TC(Transaction Coordinator)能力,客户端(TM/RM)要准确声明事务边界并正确嵌入数据源,注册与配置中心则要确保所有节点能实时发现彼此。它不像MySQL驱动那样加个JAR、配个URL就完事;它更像一个交通指挥系统——光有红绿灯(TC)不行,路口摄像头(RM)、调度中心(TM)、甚至地图导航(注册中心)缺一不可。而绝大多数人跳过了“理解契约”这一步,直接冲进“改配置”环节,结果就是各种“No available service”“branch register failed”“undo log not found”轮番轰炸。
所以这篇内容不叫“Seata安装教程”,它叫“Seata运行契约落地实录”。我会带你从零开始,亲手搭起一个最小可运行闭环:一个TC服务、两个微服务(订单+库存)、一个Nacos注册中心、一个MySQL实例。每一步都告诉你为什么必须这么配,而不是“复制粘贴即可”。比如为什么Seata Server的store.mode不能默认用file?因为file模式下TC重启后所有未完成事务状态全丢,生产环境等于裸奔;为什么客户端的service.vgroupMapping必须和tx-service-group严格一致?因为这是TC用来路由分支事务的唯一键,错一个字符就找不到归属组;为什么AT模式下@GlobalTransactional不能加在异步方法上?因为异步线程会丢失全局事务上下文,RM压根收不到分支注册指令。
关键词就藏在这套契约里:Seata Server、TC、TM、RM、AT模式、Nacos、MySQL、undo_log、事务分组、注册中心、配置中心。它们不是孤立名词,而是环环相扣的齿轮。接下来的内容,就是把这些齿轮怎么咬合、哪里容易打滑、打滑了怎么校准,全部拆开给你看。
2. Seata Server启动前的三道生死关——注册中心、配置中心、存储模式选型实战
Seata Server(即TC)不是独立运行的进程,它严重依赖外部基础设施。很多人的“安装失败”,其实根本没走到Seata代码层面,而是卡在了启动前的环境准备阶段。我见过太多人直接解压seata-server.zip,双击seata-server.bat,看到控制台输出Started SeataServer就以为大功告成,结果客户端死活连不上——因为TC压根没注册到Nacos,或者配置根本没加载进来。这三道关,缺一不可,且顺序不能乱。
2.1 第一道关:注册中心必须先于TC启动,且健康检查要过
Seata Server启动时,第一件事就是向注册中心(如Nacos)注册自己为serverAddr服务。如果注册中心没起来,或者网络不通,TC会反复重试直到超时,最终抛出No available service。但很多人忽略了关键细节:Nacos的健康检查机制必须兼容Seata的注册心跳。
以Nacos 2.2.3为例,默认健康检查是基于UDP的/nacos/v1/ns/instance/beat接口。而Seata 1.7.1客户端注册时发送的是HTTP POST请求,且携带的metadata中包含"preserved.register.source":"SEATA"标识。如果Nacos配置了严格的健康检查策略(比如要求实例必须响应特定HTTP头),Seata实例就会被标记为UNHEALTHY,即使注册成功,客户端也查不到可用实例。
实操验证步骤:
- 启动Nacos单机版:
sh startup.sh -m standalone - 访问
http://localhost:8848/nacos,登录后进入“服务管理”→“服务列表” - 启动Seata Server前,确认页面为空
- 执行
sh seata-server.sh -p 8091 -h 127.0.0.1 -n 1(注意-h必须是客户端能访问到的IP,不能写localhost) - 刷新Nacos页面,等待10秒,观察
serverAddr服务状态是否为UP
提示:如果状态一直是
DOWN,立刻检查Nacos日志logs/nacos.log,搜索failed to update instance status。常见原因是Seata注册时携带的ip字段与Nacos实际监听IP不一致。解决方案:在seata/conf/application.yml中显式配置nacos.ip,或启动参数加-h 192.168.1.100(换成你本机真实局域网IP)。
2.2 第二道关:配置中心加载失败,TC会静默降级为file模式
Seata Server启动时,第二件事是从配置中心拉取file.conf和registry.conf。很多人以为只要registry.conf里写了Nacos地址就行,其实不然。Seata的配置加载有严格优先级:JVM参数 > 环境变量 >registry.conf>file.conf。而最关键的是——如果配置中心连接失败,Seata不会报错退出,而是自动fallback到file.conf中定义的store.mode=file。这意味着你明明配了store.mode=db,结果TC却在用文件存事务日志,重启后所有进行中的事务全丢。
验证方法极其简单:启动TC后,立刻执行curl -X GET "http://localhost:8091/actuator/seata"(需开启Actuator)。返回JSON中storeMode字段值就是当前生效模式。如果是file,说明配置中心加载失败。
排查链路:
- 检查
seata/conf/registry.conf中nacos.server-addr是否正确(格式必须是127.0.0.1:8848,不能带http://) - 检查Nacos是否开启了鉴权(
nacos.core.auth.enabled=true)。如果开了,Seata必须在registry.conf中配置username和password - 检查
seata/conf/file.conf中service.vgroupMapping.my_test_tx_group是否与客户端配置完全一致(包括大小写和下划线)
注意:
vgroupMapping的value值(如default)必须与Nacos配置中心里的dataId前缀匹配。例如,若你在Nacos中创建了dataId=seataServer.properties,那么vgroupMapping的value就必须是seataServer,否则TC无法加载该配置。
2.3 第三道关:存储模式选型——DB模式必须手建表,file模式仅限单机演示
Seata的store.mode决定了TC如何持久化全局事务、分支事务、锁等元数据。官方文档说支持file、db、redis三种,但生产环境唯一可选的是db。原因很现实:file模式下所有数据存在本地磁盘,TC集群部署时各节点数据不同步,事务状态无法共享;redis模式虽快,但Redis本身不保证强一致性,极端情况下可能丢事务日志。
DB模式要求你手动在MySQL中创建三张表:
-- 全局事务表 CREATE TABLE IF NOT EXISTS `global_table` ( `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_gmt_modified_status` (`gmt_modified`, `status`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- 分支事务表 CREATE TABLE IF NOT EXISTS `branch_table` ( `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- 全局锁表 CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(96), `transaction_id` BIGINT, `branch_id` BIGINT, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;踩坑实录:某次我在Docker中部署MySQL 8.0,执行建表SQL后TC启动报
Unknown column 'gmt_create' in 'field list'。排查发现是MySQL 8.0默认时区为SYSTEM,而Seata JDBC驱动要求gmt_create字段类型为DATETIME,但某些JDBC版本对DATETIME(6)支持不完善。解决方案:将建表语句中所有DATETIME(6)改为DATETIME,并在seata/conf/file.conf的store.db配置块中添加url = "jdbc:mysql://mysql:3306/seata?useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true"。
这三道关,本质是Seata对运行环境的“契约声明”。绕过任何一道,后续所有功能都是空中楼阁。我建议你严格按此顺序操作:先启Nacos并验证Web界面,再启MySQL并建库建表,最后启动Seata Server并用Actuator接口确认storeMode=db且status=UP。少一步,后面全是坑。
3. 客户端集成的四个致命细节——从Spring Boot Starter到undo_log自动生成
客户端(TM/RM)集成看似简单:加依赖、配属性、加注解。但恰恰是这些“最简单”的步骤,埋着最多隐形炸弹。我统计过某技术社区近三个月关于Seata的提问,72%集中在客户端配置错误,其中又以@GlobalTransactional失效、undo_log表缺失、数据源代理失败为前三名。这些问题的根源,不是代码写错了,而是对Seata AT模式的工作原理理解偏差。
3.1 依赖版本必须严格对齐——Spring Boot 2.7.x与Seata 1.7.1的兼容性陷阱
Seata官方提供了seata-spring-boot-starter,但它的版本号与Spring Boot、MyBatis、Druid等生态组件存在强耦合。例如,Seata 1.7.1的starter明确要求Spring Boot 2.6.x ~ 2.7.x,如果你用的是Spring Boot 3.0+(基于Jakarta EE 9),直接引入会报java.lang.NoClassDefFoundError: javax/servlet/Filter——因为Jakarta已将javax.*包全迁移到jakarta.*。
版本对齐表(经实测验证):
| Spring Boot 版本 | Seata 版本 | Starter 版本 | 关键注意事项 |
|---|---|---|---|
| 2.5.15 | 1.4.2 | 1.4.2 | 需额外排除spring-boot-starter-logging冲突 |
| 2.7.18 | 1.7.1 | 1.7.1 | 当前最稳组合,推荐 |
| 3.0.12 | 2.0.0-RC1 | 2.0.0-RC1 | 需升级至Java 17+,且MyBatis 3.5.13+ |
实操中,我曾在一个Spring Boot 2.7.18项目中误引入seata-all-1.6.1.jar,导致DataSourceProxy初始化失败。错误日志只显示Failed to instantiate [io.seata.rm.datasource.DataSourceProxy],没有堆栈。最终定位方法是:在@SpringBootApplication类上加@EnableAutoConfiguration(exclude = {SeataAutoConfiguration.class}),然后手动配置DataSourceProxyBean,通过断点发现SeataDataSourceBeanPostProcessor的postProcessAfterInitialization方法被跳过——因为1.6.1的starter不识别2.7.18的@ConditionalOnClass条件。
解决方案:永远使用
seata-spring-boot-starter,而非seata-all。在pom.xml中:<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.7.1</version> </dependency>并确保父POM中
spring-boot-dependencies版本为2.7.18。
3.2@GlobalTransactional的生效边界——为什么加在Service层却没效果?
这是最高频的误解:认为只要在Service方法上加@GlobalTransactional,整个方法内所有数据库操作就自动纳入全局事务。真相是:该注解只对当前Spring容器内的Bean方法调用生效,且要求目标方法必须由Spring AOP代理对象调用。
典型失效场景:
- 场景1:在Controller中直接
new OrderService().createOrder(),而非注入@Autowired OrderService orderService。此时createOrder()是普通Java方法调用,AOP代理不生效。 - 场景2:
OrderService内部方法A调用方法B,且B也加了@GlobalTransactional。由于Spring默认使用JDK动态代理(基于接口),如果B方法是private或final,代理对象无法重写该方法,事务失效。 - 场景3:方法上同时有
@Transactional(Spring本地事务)和@GlobalTransactional。此时Seata会尝试嵌套事务,但AT模式不支持嵌套,会导致BranchTransactionException。
验证方法:在@GlobalTransactional方法内,打断点查看RootContext.getXID()是否为非空字符串。如果为空,说明全局事务上下文未建立。
实战技巧:在
application.yml中开启Seata日志调试:logging: level: io.seata: DEBUG io.seata.rm: DEBUG当方法执行时,日志中会出现
Begin new global transaction和Register branch successfully,这才是真正生效的标志。
3.3 undo_log表必须由客户端自动创建——手动建表反而是大忌
AT模式的核心是“自动补偿”。Seata RM在执行SQL前,会解析SQL获取主键、表名、操作类型,然后在业务SQL执行后,自动生成对应的undo_log记录(即回滚镜像)。这个过程要求RM能动态创建undo_log表。因此,你绝不能手动在MySQL中创建undo_log表。
原因有二:
- 手动建表时,你无法预知Seata生成的
undo_log表结构。不同版本Seata的undo_log字段略有差异(如1.4.2含log_status,1.7.1已移除),手动建错字段类型会导致插入失败。 - Seata RM在启动时会检查
undo_log表是否存在。如果不存在,它会自动执行建表SQL;如果存在但结构不匹配,它会尝试ALTER TABLE,但某些DDL操作(如修改TEXT字段长度)在MySQL中可能被拒绝。
正确做法:确保业务数据库用户拥有CREATE TABLE权限,并在application.yml中配置:
seata: >GRANT CREATE, ALTER, INSERT, SELECT ON your_db.* TO 'your_user'@'%'; FLUSH PRIVILEGES;3.4 数据源必须被Seata代理——Druid、HikariCP的配置差异
Seata RM要拦截SQL,必须接管原始数据源。Spring Boot Starter会自动寻找DataSource类型的Bean并包装为DataSourceProxy。但如果你用了Druid或HikariCP,且配置方式不标准,代理就会失败。
Druid标准配置(必须用@Bean声明):
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.druid") public DataSource druidDataSource() { return DruidDataSourceBuilder.create().build(); } }如果写成@Value注入属性再new DruidDataSource(),Starter无法识别该Bean,导致RM拿到的是原始Druid数据源,SQL拦截失效。
HikariCP同理,但要注意:HikariCP的HikariDataSource实现了DataSource接口,但其getConnection()返回的Connection对象不支持Seata的ConnectionProxy包装。解决方案是强制使用HikariDataSource的子类:
@Bean @ConfigurationProperties("spring.datasource.hikari") public HikariDataSource hikariDataSource() { return new HikariDataSource(); // 必须new,不能用Builder }关键验证点:在业务方法中,打印
dataSource.getClass().getName()。正确应为io.seata.rm.datasource.DataSourceProxy;如果还是com.alibaba.druid.pool.DruidDataSource,说明代理失败。
这四个细节,每一个都对应一个“为什么我的@GlobalTransactional没用”的真实案例。它们不是配置错误,而是对Seata工作流的误读。记住:Seata的AT模式不是魔法,它是基于SQL解析、数据源代理、undo_log生成的一套精密流水线。任何一个环节没对齐,整条线就停摆。
4. 常见故障的完整排查链路——从“No available service”到“undo log not found”
线上环境遇到Seata报错,最忌讳的就是百度错误码然后改配置。真正的高手,是顺着Seata的日志流,一层层剥开问题本质。我整理了一套标准化排查路径,覆盖95%的高频问题。这套路径不是“答案清单”,而是“思考地图”,让你下次遇到新错误也能快速定位。
4.1 “No available service”——不是网络问题,是服务发现链断裂
这个错误90%不是网络不通,而是服务发现流程中某个环节断开。排查必须按顺序:
Step 1:确认TC是否真的注册成功
- 登录Nacos控制台 → 服务列表 → 查找
serverAddr服务 - 点击服务名 → 查看“实例列表”,确认
healthy列是否为true - 如果为
false,点击实例右侧“详情”,查看metadata中是否有"preserved.register.source":"SEATA"。没有则TC注册失败
Step 2:确认客户端是否正确订阅
- 在客户端应用中,加日志:
@PostConstruct public void checkRegistry() { RegistryFactory.getInstance().lookup("default"); System.out.println("Registry lookup success"); } - 如果抛
NoAvailableServiceException,说明客户端连不上Nacos,检查registry.conf中nacos.server-addr
Step 3:确认事务分组映射是否一致
- 客户端
application.yml中:seata: service: vgroup-mapping: my_test_tx_group: default # ← 这个default必须与TC的vgroupMapping值一致 - TC的
file.conf中:service.vgroupMapping.my_test_tx_group = default - Nacos中
dataId=seataServer.properties的配置里,必须有service.vgroupMapping.my_test_tx_group=default
排查工具:用
curl直连Nacos API验证:curl "http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=serverAddr" # 返回JSON中instances数组长度必须>0,且每个instance的metadata包含seata标识
4.2 “Branch register failed”——RM没找到TC,或SQL解析失败
分支注册失败,意味着RM(资源管理器)无法向TC(事务协调器)上报自己的分支事务。原因分两类:
网络侧原因(占30%)
- RM与TC之间网络不通。用
telnet 127.0.0.1 8091测试TC端口 - RM配置的
service.grouplist指向了错误IP。grouplist只在file注册模式下生效,Nacos模式下必须用vgroup-mapping
SQL侧原因(占70%,最易忽略)
- RM执行的SQL无法被Seata解析。AT模式只支持标准DML(INSERT/UPDATE/DELETE),不支持:
- 多表关联UPDATE(
UPDATE t1 JOIN t2 ON ... SET t1.a=1) - 存储过程调用(
CALL proc_name()) INSERT ... SELECT(除非SELECT部分无子查询)
- 多表关联UPDATE(
- 表没有主键。Seata需要主键生成undo_log的
beforeImage和afterImage,无主键表会报Table without primary key
验证方法:在application.yml中开启SQL解析日志:
logging: level: io.seata.rm.datasource.sql.SQLRecognizer: DEBUG执行业务SQL后,日志中会出现Parse SQL: xxx,如果出现Unsupported SQL type,说明SQL不被支持。
4.3 “Undo log not found”——不是表丢了,是镜像生成失败
这个错误常让人误以为undo_log表被删了,其实99%的情况是:业务SQL执行成功了,但Seata没能生成对应的undo_log记录。
根本原因有三:
- 事务未提交:业务方法抛异常但未被
@GlobalTransactional捕获(如try-catch吞掉了异常),导致全局事务回滚,但RM没收到回滚指令,undo_log未生成 - SQL未被拦截:数据源未被
DataSourceProxy代理(见3.4节),SQL直接走原生JDBC,Seata完全不知情 - 主键冲突:业务表主键是UUID,但
undo_log表的branch_id是BIGINT,Seata生成镜像时因主键类型不匹配失败
诊断步骤:
- 查看
undo_log表数据量:SELECT COUNT(*) FROM undo_log;。如果为0,说明镜像从未生成 - 检查RM日志中是否有
Insert undo log sql字样。没有则SQL未被拦截 - 在业务方法中,手动执行
SELECT * FROM undo_log WHERE xid='xxx'(xxx为RootContext.getXID()),确认是否有记录
终极验证法:在
DataSourceProxy的execute方法中加断点,确认业务SQL执行前后,undoLogManager是否被调用。这是判断AT模式是否真正工作的黄金标准。
4.4 “Global transaction is rollbacking”后业务数据未回滚——补偿逻辑被绕过
最危险的错误:日志显示全局事务已回滚,但数据库数据却没变。这说明补偿操作(执行undo_log中的反向SQL)根本没有执行。
核心原因只有一个:RM在回滚阶段,无法根据undo_log中的beforeImage和afterImage生成正确的反向SQL。
典型场景:
- 业务表结构变更:回滚时表已增加新字段,但undo_log中
beforeImage不含该字段,导致UPDATE语句语法错误 - 字段类型变更:如
VARCHAR(50)改为VARCHAR(100),undo_log中beforeImage的值长度超限 - 主键被修改:业务逻辑中手动更新了主键值,但undo_log中
beforeImage的主键与当前行不匹配,Seata找不到要更新的记录
解决方案:启用Seata的undo.log.validation(默认关闭):
seata: rm: report-success-enable: true undo: log-validation: true # 开启校验,失败时抛异常而非静默开启后,回滚失败会明确报Data validation failed,并打印beforeImage与当前数据的差异。
这套排查链路的价值,在于它把模糊的错误提示,转化成了可验证的原子动作。每次遇到新问题,你都可以问自己:这是注册问题?发现问题?SQL解析问题?还是镜像生成问题?沿着这个链条往下走,比盲目改配置高效十倍。
5. 生产环境必须加固的五个安全阀——从单点故障到数据一致性兜底
把Seata跑通只是第一步,让它在生产环境扛住流量、不出数据错,才是真正的挑战。我参与过的三个高并发项目,都经历过这样的教训:测试环境完美,上线后第3天凌晨,因TC节点宕机,导致数百笔订单状态不一致。后来我们总结出五道“安全阀”,每一道都对应一个血泪教训。
5.1 TC必须集群部署——单点是最大的技术债
Seata官方文档说“TC可以单点部署”,但这是对Demo场景的妥协。生产环境单点TC,等于把所有事务命运押在一台机器上。一旦TC宕机,所有新发起的全局事务都会失败,而正在执行的事务会因心跳超时被TC强制回滚——但此时RM可能已提交本地事务,造成数据不一致。
正确方案:TC集群 + Nacos集群。最低配置为3节点:
- 节点1:
sh seata-server.sh -p 8091 -h 192.168.1.101 -n 1 - 节点2:
sh seata-server.sh -p 8092 -h 192.168.1.102 -n 2 - 节点3:
sh seata-server.sh -p 8093 -h 192.168.1.103 -n 3
关键配置:seata/conf/registry.conf中nacos.cluster必须设为true,且所有TC节点指向同一个Nacos集群。这样Nacos会将serverAddr服务注册为多个健康实例,客户端通过负载均衡自动选择。
实测数据:某电商项目TC集群3节点,单节点故障时,客户端切换时间<200ms,期间事务失败率<0.3%。而单点TC故障,平均恢复时间15分钟,期间所有事务失败。
5.2 undo_log表必须分区——避免单表过大拖垮MySQL
AT模式下,每笔分支事务都会向undo_log表插入1~3条记录。高并发场景下,单日可能产生百万级记录。如果undo_log是普通InnoDB表,随着数据增长,SELECT和DELETE操作会越来越慢,最终拖垮整个MySQL。
解决方案:按时间分区。MySQL 5.7+支持RANGE分区:
ALTER TABLE undo_log PARTITION BY RANGE (TO_DAYS(gmt_create)) ( PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')), PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')), PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')), PARTITION p_future VALUES LESS THAN MAXVALUE );每月初自动执行ALTER TABLE undo_log REORGANIZE PARTITION p_future INTO (...),并删除三个月前的分区(ALTER TABLE undo_log DROP PARTITION p202401)。
注意:分区字段必须是
gmt_create,且该字段不能为NULL。Seata 1.7.1默认插入时会设置gmt_create=NOW(),符合要求。
5.3 必须开启TC的事务超时自动清理——防止长事务堆积
开发人员常忘记设置@GlobalTransactional(timeoutMills=30000),导致全局事务长时间挂起。TC内存中会累积大量GlobalSession对象,最终OOM。更危险的是,这些长事务会一直持有数据库行锁,阻塞其他业务。
TC内置了超时清理机制,但默认关闭。必须在seata/conf/file.conf中显式开启:
service { # 全局事务超时时间,单位毫秒 enable-dead-lock-detect = true # 自动检测并清理超时事务 enable-timeout-check = true # 清理周期,单位毫秒 timeout-check-interval = 30000 }开启后,TC会每30秒扫描一次global_table,将status=2(Committing)且begin_time超过timeoutMills的事务强制置为status=3(CommitFailed),并触发回滚。
5.4 客户端必须配置降级开关——当Seata不可用时优雅退化
再完善的架构也无法100%避免Seata故障。此时,业务不能跟着一起挂。必须设计降级策略:当Seata不可用时,自动切换为本地事务,牺牲强一致性,保业务可用性。
实现方式:Spring Cloud Alibaba Sentinel + Seata整合。在application.yml中:
seata: enabled: true # 降级开关,可通过Apollo/Nacos动态修改 fallback-enabled: true sentinel: datasource: ds1: nacos: server-addr: 127.0.0.1:8848 >[ { "resource": "seata-tc-connect", "count": 0, "grade": 0 } ]业务代码中:
@SentinelResource(value = "seata-tc-connect", fallback = "localFallback") public String createOrder() { return globalTransactionTemplate.execute(status -> { // 业务逻辑 return "success"; }); } public String localFallback(BlockException ex) { // 降级逻辑:用Spring @Transactional执行本地事务 return localTransactionTemplate.execute(status -> { // 相同业务逻辑 return "success"; }); }5.5 必须建立事务一致性监控大盘——用数据说话
最后也是最重要的一道阀:监控。没有监控的分布式事务,就像没有仪表盘的飞机。
核心监控指标(Prometheus + Grafana):
seata_global_transaction_total{status="committed"}:成功提交数seata_global_transaction_total{status="rollbacked"}:成功回滚数seata_global_transaction_total{status="timeout"}:超时数(需告警)seata_branch_transaction_total{status="registered"}:分支注册数seata_undo_log_size_bytes:undo_log表总大小(需告警>10GB)
告警规则示例:
rate(seata_global_transaction_total{status="timeout"}[5m]) > 0.01:5分钟内超时率>1%,立即告警sum(seata_undo_log_size_bytes) by (instance) > 10737418240:单实例undo_log大小>10GB,需人工介入清理
我的个人经验:上线后第一周,每天花15分钟看这个大盘。你会发现:超时事务多,说明业务方法太慢;分支注册失败多,说明SQL有不支持语法;undo_log增长快,说明有长事务未关闭。这些数据,比任何日志都真实。
这五道安全阀,不是锦上添花,而是生产环境的生存底线。它们背后是一个朴素真理:分布式事务不是银弹,它是用复杂度换一致性。而我们的工作,就是把这份复杂度,变成可监控、可降级、可兜底的确定性。
6. 最后分享一个真实技巧:如何用Seata的AT模式实现“最终一致性”补偿
很多团队纠结一个问题:Seata AT模式是强一致,但业务场景其实只需要最终一致(比如订单创建后发短信,短信服务失败不影响订单)。这时硬上AT,反而增加性能损耗和运维负担。
我的方案是:用AT模式做“事务锚点”,用消息队列做“最终一致”。具体来说,把消息发送封装成一个独立的、可重试的本地事务,由Seata保证其与主业务的原子性,而消息消费端负责幂等和重试。
以订单创建为例:
@GlobalTransactional public void createOrder(Order order) { // 1. 创建订单(本地事务) orderMapper.insert(order); // 2. 发送MQ消息(本地事务,与订单同一DB) mqMessageMapper.insert(MqMessage.builder() .topic("order.created") .payload(JSON.toJSONString(order)) .status("pending") // 待发送 .build()); }另起一个定时任务,扫描mq_message表中status=pending的消息,调用MQ Producer发送。发送成功后,更新status=sent;失败则重