1. 分布式事务到底在解决什么问题
1.1 从一个常见的业务场景说起
假设你负责一个电商系统的订单模块。用户下单这个动作,表面上看只是一次“提交订单”的点击,但背后至少涉及三个独立的服务:订单服务要写入一条订单记录,库存服务要扣减对应商品的库存,账户服务要冻结用户相应的余额。在单体应用时代,这三步操作可以放在同一个本地数据库事务里,要么全成功,要么全回滚,简单直接。
但当你把系统拆成微服务之后,情况就完全变了。三个服务各自连接着独立的数据库,本地事务只能管住自己那一亩三分地。订单写进去了,库存扣减的时候网络超时了,账户冻结又成功了——这时候数据就乱了。用户看到订单已创建,但库存没扣,余额却少了。这种问题在分布式系统里几乎是必然会遇到的,不是“会不会发生”,而是“什么时候发生”。
分布式事务要解决的核心问题就是:在多个独立服务、多个独立数据源之间,保证一组操作要么全部成功,要么全部失败。听起来简单,做起来极其复杂,因为网络不可靠、节点可能宕机、消息可能丢失或重复。
1.2 为什么选择Seata
解决分布式事务的方案有很多种,比如两阶段提交(2PC)、TCC(Try-Confirm-Cancel)、可靠消息最终一致性、Saga模式等。每种方案都有自己的适用场景和代价。两阶段提交强一致但性能差、阻塞严重;TCC需要业务改造量大,每个接口都要写三个方法;可靠消息方案依赖消息中间件,实现复杂度也不低。
Seata的思路是在这些方案之间找一个平衡点。它最早是阿里内部的一个项目,后来开源出来,现在已经是分布式事务领域最主流的框架之一。Seata提供了四种事务模式:AT模式、TCC模式、Saga模式和XA模式。其中AT模式是对业务侵入最小的,也是绝大多数团队首选的方案。
AT模式的核心思想是:通过拦截业务SQL,自动生成反向补偿SQL(回滚日志),在全局事务提交或回滚时自动执行。业务代码几乎不需要改动,只需要加一个注解,剩下的交给Seata框架处理。这对于已经上线的系统来说,改造成本极低。
1.3 这篇文章适合谁看
如果你正在做微服务架构,遇到了跨服务数据一致性的问题,或者团队正在选型分布式事务方案,那这篇内容就是为你准备的。我会从零开始,把Seata的配置、集成、踩坑经验完整地讲一遍。不管你是刚接触分布式事务的新手,还是已经用过Seata但遇到了一些疑难问题的老手,都能从中找到有用的东西。
接下来的内容会覆盖:Seata的整体架构和核心概念、Server端的部署配置、Client端(也就是业务服务)的集成步骤、AT模式的工作原理、实际项目中的配置示例、常见问题排查,以及一些我在实际使用中总结出来的经验技巧。
2. Seata核心架构与关键概念拆解
2.1 三个核心角色:TC、TM、RM
Seata的架构里有三个关键角色,理解它们是理解整个框架的前提。
TC(Transaction Coordinator)——事务协调者。它是Seata Server,独立部署的一个服务。TC负责维护全局事务的状态,协调各个分支事务的提交或回滚。你可以把它想象成一个“总指挥”,所有的事务决策都由它来做。
TM(Transaction Manager)——事务管理者。它嵌入在业务服务里,负责定义全局事务的边界。具体来说,就是标注@GlobalTransactional注解的那个服务,它会在方法开始时向TC申请开启一个全局事务,方法结束时根据执行结果通知TC提交或回滚。
RM(Resource Manager)——资源管理者。它也嵌入在业务服务里,负责管理分支事务。每个参与全局事务的数据库操作都是一个分支事务,RM会向TC注册分支事务,上报执行状态,并在收到TC指令后执行提交或回滚。
三者之间的关系可以用一个简单的流程来描述:TM向TC申请开启全局事务,TC生成一个全局唯一的XID返回给TM;TM把这个XID传递给下游服务;每个服务的RM在执行SQL时,向TC注册分支事务,并把XID带上;所有分支执行完毕后,TM根据结果通知TC,TC再驱动所有RM完成提交或回滚。
2.2 AT模式的工作原理
AT模式是Seata最常用的模式,全称是Automatic Transaction,也就是自动事务。它的“自动”体现在哪里?体现在你不需要手写回滚逻辑,Seata会自动帮你生成。
具体的工作流程是这样的:当RM执行一条业务SQL时,Seata会拦截这条SQL,解析出它的语义,然后做两件事。第一,在业务SQL执行之前,查询出受影响的数据行的当前值,保存为“前镜像”(before image)。第二,在业务SQL执行之后,查询出受影响的数据行的新值,保存为“后镜像”(after image)。这两个镜像加上SQL本身的信息,一起组成一条回滚日志,写入到undo_log表里。
如果全局事务最终提交,Seata会异步删除这些回滚日志,清理工作就完成了。如果全局事务需要回滚,Seata会根据回滚日志里的前镜像,生成反向SQL,把数据恢复到修改之前的状态。比如原来是UPDATE stock SET count = count - 1 WHERE id = 1,回滚时就会执行UPDATE stock SET count = count + 1 WHERE id = 1。
这里有一个关键点:回滚日志和业务SQL必须在同一个本地事务里提交。这是AT模式能保证数据一致性的基础。如果业务SQL提交了但回滚日志没写进去,那回滚的时候就找不到依据了。Seata通过数据源代理来保证这一点,它会拦截本地事务的提交,在提交之前先把回滚日志写好。
2.3 全局锁与写隔离
AT模式还有一个重要的机制:全局锁。当多个全局事务同时操作同一条数据时,可能会出现脏写的问题。比如事务A修改了某行数据但还没提交,事务B也来修改同一行数据,如果B先提交了,A回滚的时候就会把B的修改覆盖掉。
Seata的解决方案是在TC层面维护一把全局锁。当一个分支事务要修改某行数据时,需要先向TC申请这把锁。拿到锁之后才能执行SQL并提交本地事务。如果锁被其他事务占用了,当前事务会重试等待,直到拿到锁或者超时。
这个机制保证了写隔离:不同全局事务之间的写操作是串行的,不会互相干扰。但代价是性能会有所下降,因为锁的申请和释放都需要和TC通信。在实际项目中,如果并发量很大,全局锁可能成为瓶颈。这时候可以考虑把热点数据拆分,或者改用其他事务模式。
2.4 四种事务模式的对比
Seata支持四种事务模式,它们各有优劣,适用于不同的场景。
| 模式 | 侵入性 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|---|
| AT | 极低 | 最终一致 | 较高 | 大多数业务场景,尤其是已有系统改造 |
| TCC | 高 | 最终一致 | 高 | 对性能要求高、需要自定义补偿逻辑的场景 |
| Saga | 中 | 最终一致 | 高 | 长流程业务,需要编排多个步骤的场景 |
| XA | 低 | 强一致 | 低 | 对一致性要求极高、可以接受性能损失的场景 |
AT模式是默认选择,因为它的改造成本最低。TCC模式需要你把每个业务接口拆成Try、Confirm、Cancel三个方法,开发量大但控制力最强。Saga模式适合那种流程很长、步骤很多的业务,比如旅游预订(订机票、订酒店、租车、买保险),每一步都有对应的补偿操作。XA模式是传统的两阶段提交,强一致但性能最差,一般只在特殊场景下使用。
对于大多数团队来说,从AT模式开始是最务实的选择。它能覆盖80%以上的场景,而且后续如果需要切换到其他模式,Seata的API是统一的,迁移成本可控。
3. Seata Server端部署与配置实操
3.1 环境准备与版本选择
在开始配置之前,先把环境准备好。Seata Server需要Java运行环境,建议使用JDK 8或JDK 11,这两个版本经过大量生产验证,稳定性最好。JDK 17虽然也支持,但在某些旧版本的Seata上可能会遇到兼容性问题,如果团队没有强制要求,建议先用JDK 8或11。
版本选择上,Seata的版本迭代比较快,建议选择最近一年内的稳定版本。太老的版本可能有已知的bug,太新的版本可能文档和社区支持还不够完善。下载地址在Seata的官方GitHub Release页面,选择seata-server-{version}.tar.gz或.zip包即可。
存储模式方面,Seata Server支持三种:file、db、redis。file模式最简单,数据存在本地文件里,适合开发和测试环境。db模式把事务数据存到数据库里,适合生产环境,因为支持集群部署。redis模式性能好,但需要额外维护Redis集群。生产环境推荐db模式,下面也会以db模式为例来配置。
3.2 数据库表结构初始化
db模式需要在数据库中创建三张表:global_table、branch_table和lock_table。这三张表分别存储全局事务、分支事务和全局锁的信息。
建表SQL在Seata的源码仓库的script/server/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;branch_table存储分支事务信息,lock_table存储全局锁信息。这三张表不需要业务代码直接操作,是Seata Server内部使用的。建表的时候注意字符集用utf8mb4,因为XID可能包含特殊字符。
注意:建表时不要修改表名和字段名,Seata Server的代码里是硬编码的。如果确实需要改,得同步修改Server端的配置,比较麻烦,不建议这么做。
3.3 Server端配置文件详解
Seata Server的配置文件在conf目录下,主要有两个:registry.conf和file.conf(新版本可能叫application.yml)。registry.conf配置注册中心和配置中心的类型,file.conf配置Server端的各项参数。
先看registry.conf:
registry { type = "nacos" nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "" cluster = "default" username = "nacos" password = "nacos" } } config { type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" dataId = "seataServer.properties" } }这里注册中心用了Nacos,这是目前最常用的选择。如果你的团队用的是Eureka、Consul或Zookeeper,把type改掉,然后配置对应的连接信息即可。配置中心也用了Nacos,这样Server端的参数可以动态调整,不用重启。
再看file.conf里的关键配置:
store { mode = "db" db { datasource = "druid" dbType = "mysql" driverClassName = "com.mysql.cj.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai" user = "seata" password = "seata123" minConn = 5 maxConn = 100 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 } }mode设为db表示使用数据库存储。url指向刚才建表的数据库。minConn和maxConn是连接池的最小和最大连接数,根据实际并发量调整。queryLimit是每次查询的最大记录数,默认100,如果事务量很大可以适当调大。
3.4 启动参数与JVM调优
Seata Server的启动脚本在bin目录下,Linux用seata-server.sh,Windows用seata-server.bat。启动时可以指定一些参数,比如端口、主机名、日志级别等。
sh seata-server.sh -p 8091 -h 127.0.0.1 -m db-p指定监听端口,默认是8091。-h指定注册到注册中心的IP,如果不指定会自动获取本机IP,但在多网卡环境下可能会取错,建议显式指定。-m指定存储模式,覆盖配置文件里的设置。
JVM调优方面,Seata Server本身的内存消耗不大,但事务量大的时候需要足够的堆内存。建议设置-Xmx2g -Xms2g,新生代和老年代的比例用默认的就行。如果事务量特别大,可以适当增加堆内存,同时调整GC参数,比如用G1收集器:
JAVA_OPTS="-Xmx4g -Xms4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200"日志方面,Seata Server的日志默认输出到logs目录下,包括seata.log和registry.log等。生产环境建议把日志级别调到INFO,调试时可以调到DEBUG,但DEBUG级别日志量很大,会快速消耗磁盘空间,调试完记得改回来。
3.5 集群部署注意事项
生产环境一般需要部署多个Seata Server实例组成集群,避免单点故障。集群部署时,所有实例连接同一个数据库,通过数据库的锁机制来协调。注册中心里会注册多个实例,Client端会随机选择一个进行通信。
集群部署有几个注意点。第一,所有Server实例的transactionServiceGroup配置必须一致,否则Client端可能找不到正确的Server。第二,数据库连接池的大小要合理设置,因为多个Server实例会同时竞争数据库连接,如果连接池太小,可能会出现获取连接超时的情况。第三,时间同步很重要,Server之间、Server和Client之间的时间差不能太大,否则事务超时判断会出问题。建议所有机器都开启NTP时间同步。
4. 业务服务集成Seata的完整步骤
4.1 引入依赖与版本对齐
业务服务集成Seata的第一步是引入依赖。如果你用的是Spring Cloud Alibaba,可以直接引入spring-cloud-starter-alibaba-seata,它会自动帮你管理Seata的版本。如果不用Spring Cloud Alibaba,那就直接引入seata-spring-boot-starter。
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.7.0</version> </dependency>版本对齐是一个容易踩坑的地方。Seata Client的版本必须和Server的版本兼容,最好保持一致。如果Server用的是1.7.0,Client也用1.7.0。跨大版本可能会有协议不兼容的问题,比如1.4和1.7之间的通信协议就有变化。
另外,如果你用的是Spring Cloud Alibaba,它内部已经依赖了某个版本的Seata,你再显式引入seata-spring-boot-starter可能会导致版本冲突。这时候可以用mvn dependency:tree查看依赖树,把冲突的版本排除掉。
4.2 配置文件编写
Client端的配置主要在application.yml里,核心配置项如下:
seata: enabled: true application-id: order-service tx-service-group: my_tx_group service: vgroup-mapping: my_tx_group: default grouplist: default: 127.0.0.1:8091 registry: type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: "" username: nacos password: nacos config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: "" username: nacos password: nacos >@Configuration public class SeataDataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { return new DruidDataSource(); } @Primary @Bean public DataSource dataSource(DataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } }关键是用DataSourceProxy把原始数据源包一层。这样Seata才能拦截到SQL执行。如果忘记做代理,全局事务回滚时会发现没有回滚日志,导致回滚失败。这是新手最容易踩的坑之一。
4.4 undo_log表的创建
每个参与全局事务的数据库都需要创建undo_log表,用来存储回滚日志。建表SQL如下:
CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4;注意rollback_info字段是LONGBLOB类型,因为回滚日志可能很大,尤其是批量更新的时候。log_created和log_modified用了DATETIME(6),精确到微秒,这是为了在并发场景下更准确地排序。
注意:
undo_log表不需要在每个数据库都建,只需要在参与全局事务的数据库建。比如订单库、库存库、账户库都需要建,但一些不参与事务的库就不用建。
4.5 全局事务注解的使用
配置完成后,在业务方法上加@GlobalTransactional注解就能开启全局事务:
@Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private StockFeignClient stockFeignClient; @Autowired private AccountFeignClient accountFeignClient; @GlobalTransactional(name = "create-order", rollbackFor = Exception.class) public void createOrder(OrderDTO orderDTO) { // 1. 创建订单 orderMapper.insert(orderDTO); // 2. 扣减库存 stockFeignClient.deductStock(orderDTO.getProductId(), orderDTO.getQuantity()); // 3. 冻结余额 accountFeignClient.freezeAmount(orderDTO.getUserId(), orderDTO.getAmount()); } }name属性给全局事务取个名字,方便在日志和监控里识别。rollbackFor指定哪些异常触发回滚,一般设为Exception.class,这样任何异常都会回滚。
这里有一个重要的细节:@GlobalTransactional注解的方法里,所有远程调用都必须走Seata代理的Feign客户端或RestTemplate。如果直接用了原生的HTTP客户端,XID不会传递过去,下游服务就不知道自己在哪个全局事务里,分支事务注册会失败。
4.6 XID的传递机制
XID是全局事务的唯一标识,它需要在服务调用链中传递。Seata通过拦截器自动完成这个工作。对于Feign调用,Seata提供了SeataFeignClient,它会自动把当前线程的XID放到HTTP请求头里。对于RestTemplate,Seata也提供了拦截器。
@Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.getInterceptors().add(new SeataRestTemplateInterceptor()); return restTemplate; }如果是用Dubbo做RPC调用,Seata也有对应的Dubbo Filter,在seata-spring-boot-starter里已经自动配置了,不需要额外操作。
XID传递的核心逻辑是:TM在开启全局事务时生成XID,放到当前线程的ThreadLocal里;发起远程调用时,拦截器从ThreadLocal取出XID,放到请求头;下游服务收到请求后,拦截器从请求头取出XID,放到自己的ThreadLocal里。这样整个调用链上的服务都能拿到同一个XID。
5. 实际项目中的配置示例与参数调优
5.1 一个完整的订单场景配置
假设我们有一个订单服务,需要调用库存服务和账户服务。三个服务分别连接独立的数据库。下面是完整的配置示例。
订单服务的application.yml:
server: port: 8081 spring: application: name: order-service datasource: url: jdbc:mysql://127.0.0.1:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver seata: enabled: true application-id: order-service tx-service-group: order-service_tx_group service: vgroup-mapping: order-service_tx_group: default registry: type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: SEATA_GROUP config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP >@GlobalTransactional(name = "create-order", timeoutMills = 30000, rollbackFor = Exception.class)timeoutMills设为30000表示30秒超时。这个值要根据业务的实际执行时间来定。如果业务逻辑比较复杂,涉及多个远程调用,可以适当调大。但也不能太大,否则事务卡住的时候会长时间占用全局锁,影响其他事务。
重试策略方面,Seata Client在向Server发送请求失败时会自动重试。重试次数和间隔可以在配置里调整:
seata: client: rm: report-retry-count: 5 report-success-enable: false tm: commit-retry-count: 5 rollback-retry-count: 5report-retry-count是RM上报分支事务状态的重试次数,默认5次。commit-retry-count和rollback-retry-count是TM通知TC提交或回滚的重试次数。如果网络不稳定,可以适当调大这些值。
5.3 线程池与异步调用的处理
在异步场景下,XID的传递会出问题。因为XID存在ThreadLocal里,切换到另一个线程后ThreadLocal就取不到了。比如你用@Async注解做异步调用,或者用CompletableFuture做并行调用,XID都会丢失。
解决方案是手动传递XID。Seata提供了RootContext工具类,可以获取和绑定XID:
String xid = RootContext.getXID(); CompletableFuture.runAsync(() -> { RootContext.bind(xid); try { // 业务逻辑 } finally { RootContext.unbind(); } });在异步线程开始时绑定XID,结束时解绑。这样异步线程里的Seata操作就能正确关联到全局事务了。
注意:如果异步任务是在全局事务提交之后才执行的,那就不应该绑定XID,否则会报“全局事务已结束”的错误。要确保异步任务的执行时机在全局事务的生命周期内。
5.4 批量操作的性能优化
AT模式下,每条SQL都会生成回滚日志,批量操作时回滚日志的量会很大。比如一次批量插入1000条数据,就会生成1000条回滚日志。这不仅占用数据库空间,还会拖慢事务提交速度。
优化思路有几个。第一,尽量用小批量操作代替大批量操作,比如把1000条拆成10次100条。第二,对于不需要回滚的批量操作,可以放在全局事务外面执行。第三,调整undo_log表的清理策略,Seata默认在全局事务提交后异步删除回滚日志,如果删除不及时,表会越来越大。可以配置定时任务定期清理过期的回滚日志。
DELETE FROM undo_log WHERE log_modified < DATE_SUB(NOW(), INTERVAL 7 DAY);这个清理任务建议每天凌晨执行一次,删除7天前的回滚日志。注意不要删除正在使用中的回滚日志,所以时间间隔要设得足够长。
5.5 与消息队列的配合使用
有些场景下,全局事务提交后还需要发送消息通知其他系统。如果直接在全局事务里发消息,消息可能先于事务提交到达消费者,导致消费者读到未提交的数据。正确的做法是在全局事务提交后再发送消息。
Seata本身不提供消息队列的集成,但可以通过事务钩子来实现。@GlobalTransactional注解支持配置beforeCommit和afterCommit钩子:
@GlobalTransactional(name = "create-order") public void createOrder(OrderDTO orderDTO) { // 业务逻辑 TransactionHookManager.registerHook(new TransactionHook() { @Override public void afterCommit() { // 发送消息 mqProducer.send(new OrderCreatedEvent(orderDTO)); } }); }这样消息只会在全局事务成功提交后发送,保证了消息和事务的一致性。如果事务回滚了,afterCommit不会执行,消息也就不会发出去。
6. 常见问题排查与避坑经验实录
6.1 回滚失败:undo_log找不到或数据不一致
这是最常见的问题之一。现象是全局事务回滚时,日志里报“undo_log not found”或者“data has been modified by another transaction”。原因通常有三个。
第一,数据源没有正确代理。检查你的DataSource是不是被DataSourceProxy包装了。可以在启动日志里搜索“DataSourceProxy”,如果没有相关日志,说明代理没生效。解决办法是手动配置DataSourceProxy,或者检查自动配置是否被其他配置覆盖了。
第二,undo_log表没有创建。每个参与全局事务的数据库都要建这张表,漏建了一个就会导致那个分支的回滚失败。检查方法是连到每个数据库,执行SHOW TABLES LIKE 'undo_log'确认。
第三,数据被其他事务修改了。AT模式在回滚时会检查当前数据和后镜像是否一致,如果不一致说明数据被其他事务改过了,回滚会失败。这种情况通常是因为有非Seata管理的写操作(比如直接连数据库改数据)介入了。解决办法是确保所有写操作都走Seata代理的数据源。
6.2 全局锁冲突:lock_table中的记录无法释放
全局锁冲突的表现是事务一直卡在“获取全局锁”的阶段,最后超时回滚。查看lock_table表,会发现里面有大量未释放的记录。
原因通常是某个分支事务执行时间过长,一直占着全局锁。或者某个事务回滚失败了,锁没有正确释放。排查方法是查看lock_table里的gmt_create和gmt_modified时间,找出长时间未释放的锁。
SELECT * FROM lock_table WHERE gmt_modified < DATE_SUB(NOW(), INTERVAL 10 MINUTE);如果确认这些锁对应的事务已经结束了,可以手动删除这些记录。但手动删锁是最后的手段,一定要确认对应的事务确实已经结束,否则会导致数据不一致。更好的做法是找到根本原因,比如优化慢SQL、调整超时时间、减少事务粒度。
6.3 XID传递失败:下游服务找不到全局事务
现象是下游服务报“no available service”或者“global transaction not found”。原因通常是XID没有正确传递。
排查步骤:首先检查Feign客户端是否被Seata代理了。在启动日志里搜索“SeataFeignClient”,如果没有,说明Feign的自动配置没生效。可以手动配置:
@Bean public FeignClientBuilder feignClientBuilder() { return new FeignClientBuilder(); }其次检查请求头里有没有TX_XID。可以在下游服务的Controller里打印所有请求头,看看有没有这个头。如果没有,说明上游的拦截器没生效。
还有一个容易忽略的点:如果用了网关(比如Spring Cloud Gateway),XID可能在网关层丢失。因为网关转发请求时可能不会带上自定义的请求头。解决办法是在网关里也加上Seata的拦截器,或者配置网关允许透传TX_XID头。
6.4 性能问题:事务提交慢、数据库压力大
Seata AT模式对性能的影响主要来自三个方面:回滚日志的写入、全局锁的申请、TC的通信开销。
优化手段有几个。第一,减少事务粒度,不要把不相关的操作放在同一个全局事务里。第二,避免大事务,一次操作的数据量不要太大。第三,调整TC的连接池和线程池参数,让TC能处理更高的并发。第四,考虑读写分离,把读操作放到从库,减少主库压力。
如果性能要求极高,可以考虑改用TCC模式。TCC模式不需要写回滚日志,也不需要全局锁,性能比AT模式好很多。但代价是业务改造量大,需要自己实现Try、Confirm、Cancel三个方法。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 回滚失败,undo_log not found | 数据源未代理或undo_log表未建 | 检查启动日志和数据库表 | 配置DataSourceProxy,建undo_log表 |
| 全局锁冲突,事务卡住 | 长事务占用锁或锁未释放 | 查看lock_table表 | 优化慢SQL,手动清理过期锁 |
| XID传递失败 | Feign未代理或网关丢头 | 检查请求头和启动日志 | 配置Feign拦截器,网关透传TX_XID |
| 事务提交慢 | 回滚日志量大或TC压力大 | 查看undo_log表大小和TC监控 | 减少事务粒度,调整TC参数 |
| 下游服务报no available service | 注册中心配置错误 | 检查Nacos里的服务列表 | 修正registry配置 |
| 超时回滚 | 业务执行时间超过timeout | 查看事务执行日志 | 调大timeoutMills或优化业务逻辑 |
6.6 一些踩坑后的经验之谈
不要在全局事务里做耗时操作。比如调用外部API、发送邮件、生成报表等。这些操作会拉长事务时间,增加锁冲突的概率。正确的做法是把这些操作放到事务提交后异步执行。
测试环境一定要模拟网络异常。很多分布式事务的问题只在网络抖动时才会暴露。可以用工具模拟网络延迟、丢包、超时,验证Seata的回滚机制是否正常工作。
监控和告警要配好。Seata Server提供了监控指标,可以接入Prometheus和Grafana。重点关注事务成功率、平均耗时、全局锁等待时间这几个指标。一旦发现异常,能第一时间定位问题。
版本升级要谨慎。Seata的版本迭代快,但不同版本之间的兼容性不一定好。升级前先在测试环境验证,确认所有功能正常后再上生产。升级时Server和Client要同步升级,不要只升一边。
日志级别不要长期开DEBUG。DEBUG级别的日志量非常大,一天可能产生几十GB的日志文件,不仅占磁盘,还会影响性能。排查问题时临时开一下,排查完马上改回INFO。
undo_log表的清理要定期做。虽然Seata会在事务提交后异步删除回滚日志,但在高并发场景下,删除速度可能跟不上写入速度。建议加一个定时清理任务,每天清理一次过期数据。
全局事务的命名要有意义。@GlobalTransactional(name = "create-order")比name = "tx1"好得多。出问题的时候,通过事务名能快速定位到具体的业务方法。
不要跨线程使用同一个XID。前面提到过,异步场景下需要手动绑定XID,但要注意绑定和解绑的时机。如果异步任务在全局事务结束后才执行,绑定XID会报错。确保异步任务的执行时机在全局事务的生命周期内。
数据库连接池要留足余量。Seata的RM在执行SQL时会额外查询前镜像和后镜像,相当于每条SQL要多执行两次查询。如果连接池太小,可能会出现获取连接超时的情况。建议把连接池的最大连接数调大20%到30%。
TC的存储数据库要独立。不要把Seata Server的存储数据库和业务数据库混在一起,否则业务数据库的压力会传导到TC,影响事务协调的性能。TC的数据库压力不大,但要求稳定性和低延迟。
定期检查lock_table和global_table的大小。如果这两个表增长过快,说明可能有事务泄漏或者锁未释放的问题。正常情况下,这两个表的数据量应该保持在一个稳定的范围内。
回滚日志的内容要关注。rollback_info字段存储的是序列化后的回滚日志,如果发现某条记录特别大,说明对应的SQL影响了很多行数据。这种大事务要特别关注,考虑拆分成小事务。
Seata Server的JVM参数要调优。默认的JVM参数可能不适合生产环境。建议设置-Xmx4g -Xms4g -XX:+UseG1GC,并根据实际负载调整。如果事务量很大,可以适当增加堆内存。
网络延迟对Seata的影响很大。TC和Client之间的通信、Client和数据库之间的通信,任何一环延迟高都会影响事务性能。建议把Seata Server部署在离业务服务近的机房,减少网络延迟。
不要忽略Seata的版本兼容性。Spring Cloud Alibaba、Spring Boot、Seata三者之间的版本兼容性很重要。建议参考官方文档的版本对应关系,不要随意组合版本。
测试用例要覆盖回滚场景。很多团队只测试了正常提交的场景,没有测试回滚场景。结果上线后第一次遇到回滚就出问题了。测试用例里一定要包含各种异常情况:远程调用失败、数据库超时、业务异常等。
Seata的配置要纳入版本管理。registry.conf、file.conf、application.yml这些配置文件都要放到Git里,每次修改都有记录。不要直接在服务器上改配置,否则出了问题很难追溯。
生产环境建议用db模式而不是file模式。file模式的数据存在本地文件里,Server重启后数据可能会丢失,而且不支持集群。db模式虽然配置麻烦一点,但稳定性和可靠性好得多。
Seata Server的日志要定期归档。日志文件会不断增长,建议配置logback的滚动策略,按天或按大小切割,并定期归档到冷存储。
全局事务的timeoutMills不要设得太小。设得太小会导致正常业务也被超时回滚。建议根据业务的实际执行时间来定,一般设为平均执行时间的3到5倍。
如果用了多数据源,要确保每个数据源都被代理。Seata的自动配置只会代理主数据源,其他数据源需要手动配置DataSourceProxy。漏掉一个数据源,那个数据源上的操作就不会被Seata管理。
Seata的全局锁和数据库的本地锁是两回事。全局锁是TC层面的逻辑锁,数据库的本地锁是InnoDB的行锁。两者互不干扰,但都会影响并发性能。设计业务时要同时考虑这两把锁的影响。
回滚日志的序列化方式要注意。Seata默认用Jackson做序列化,如果业务数据里有特殊类型(比如LocalDateTime),可能需要自定义序列化器。否则回滚时反序列化会失败。
Seata Server的注册中心地址要配对。如果注册中心地址配错了,Client端找不到Server,事务根本没法开启。启动Client时留意日志里有没有“register success”之类的信息。
不要在生产环境用默认的端口和密码。Seata Server默认端口是8091,数据库默认密码是空。生产环境一定要改掉这些默认值,避免安全风险。
Seata的监控指标要接入告警系统。比如事务失败率超过1%就告警,平均耗时超过5秒就告警。这样能在问题扩大之前及时发现。
定期做故障演练。模拟Seata Server宕机、数据库宕机、网络分区等故障,验证系统的容错能力。只有经过演练,才能真正放心。
文档要写清楚每个服务的tx-service-group配置。新同事加入团队时,看到一堆配置容易懵。把每个服务的配置整理成表格,注明事务分组名和对应的Server集群,能省很多沟通成本。
Seata的版本升级要关注Release Notes。每个版本的新特性、bug修复、不兼容变更都要仔细看。特别是涉及协议变更的版本,升级时要特别小心。
如果业务对一致性要求极高,可以考虑XA模式。XA模式是强一致的,但性能差。适合那种数据量小、并发低、但对一致性要求极高的场景,比如金融核心系统。
Seata不是银弹。它解决的是分布式事务的技术问题,但业务上的补偿逻辑、幂等处理、对账机制还是需要自己设计。技术框架只能保证数据最终一致,业务上的异常情况还是要靠业务代码来处理。
最后一点:多动手实践。看再多文档不如自己搭一个环境跑一遍。本地起一个Seata Server,写两个简单的服务,模拟一下正常提交和异常回滚,把整个流程走通。遇到问题再查文档、搜社区,这样学到的知识才是自己的。