1. 为什么到现在还有人折腾Nacos 1.2.0 + Seata 1.2.0这套组合
先说结论:如果你手头维护的是2020年前后立项的老项目,大概率跑的还是Nacos 1.x + Seata 1.x这套组合。我接手过一个业务系统,注册中心是Nacos 1.2.0,分布式事务一直用的Seata 1.2.0,中途没人敢动版本,因为业务代码里@GlobalTransactional拆不掉,数据源代理、undo_log表、配置中心的下发逻辑全是按老版本调的。升级新版本意味着回归测试一大堆,没那个时间和人力。
但老版本的问题在于:资料少、配置项对不上、网上搜到的解决方案经常是驴唇不对马嘴。我这次把Nacos 1.2.0整合Seata 1.2.0的全过程重新梳理了一遍,重点放在各种配置和爬坑上,包括registry.conf、file.conf、Nacos配置中心脚本、客户端依赖版本冲突、事务分组不生效等。这套流程跑通之后,不管是新装还是给老项目补事务能力,都能直接照抄。
适合看这篇文章的人有三类:一是项目里已经用了Nacos但还没接Seata,准备引入分布式事务的;二是按网上教程配了半天,启动不报错但事务就是不回滚的;三是被迫维护老版本组件,需要快速搞清配置逻辑的。如果你是准备从零搭建一套全新微服务架构,我更建议直接用Seata 1.5+版本,配置模型变化很大,没必要在老版本上花时间。
有一点必须先说清楚:Nacos 1.2.0和Seata 1.2.0的整合,说白了就是把Seata的服务端(TC)和客户端(TM、RM)都注册到Nacos上,然后把Seata自己的配置也托管到Nacos配置中心里。这里面的核心难点不在整合动作本身,而在三处:一是Seata服务端的配置怎么让Nacos认得;二是业务服务启动时怎么从Nacos拉事务配置而不是本地文件;三是事务分组名、事务服务组映射、客户端依赖版本三者必须严格对齐,任何一环错位都会出现“事务注解加了但完全没效果”的诡异问题。
这套组合里,Seata 1.2.0的TC端扮演全局事务协调者,负责记录全局事务状态、驱动分支事务提交或回滚。TM是业务代码里@GlobalTransactional注解所在的方法入口,负责开启、提交或回滚全局事务。RM是业务服务里被代理的数据源,负责执行本地事务并上报分支状态。三者通过Nacos做服务发现,通过配置中心拿配置,任何一个环路的配置错了,整个链路就会静默失败——不是报错,而是看起来一切正常,数据却写进去了。
我这次整理的爬坑记录,基本覆盖了从零配到真正生效的完整链路,每个坑都标注了原因和排查路径,不是只给结论。
2. 环境准备阶段最容易翻车的几个细节
2.1 版本匹配关系先理清楚,别盲目用最新客户端
Seata 1.2.0配套的客户端依赖,我实测下来有两套选择:如果你的服务用的是Spring Cloud Alibaba,groupId要跟Seata服务端版本严格对应,用io.seata:seata-spring-boot-starter:1.2.0;如果你是从Seata官方文档抄的配置,可能会看到com.alibaba.cloud:spring-cloud-starter-alibaba-seata这个依赖,它内部传递的Seata版本是由Spring Cloud Alibaba版本决定的,不一定恰好是1.2.0。
我踩过的坑就是这里:项目里原来用了spring-cloud-starter-alibaba-seata,里面传递下来的Seata核心包是1.1.0,而服务端部署的是1.2.0。启动时没有任何报错,但发起全局事务后,服务端日志显示“RM register failed”或者压根不打印全局事务创建日志。后来我把依赖改成显式声明io.seata:seata-spring-boot-starter:1.2.0,并把xml里对seata-all、seata-core的间接引用清理干净,问题才消失。
这个问题的根因是Seata客户端和服务端之间的协议版本不兼容。1.1.0和1.2.0在服务发现接口的请求结构上有细微差异,Nacos上都能注册成功,但TC在分配BranchId时会因为消息体字段对不上而静默丢弃。这类问题有个很实用的排查技巧:看TC端日志里有没有“handle RPC message”的记录,如果一直没有任何消息进来,优先怀疑客户端版本与服务端不一致。
2.2 数据库初始化:不只是建一张表的事
Seata 1.2.0的TC端如果选用DB模式存储事务状态(后面会讲为什么选DB而不是File),必须在TC所在数据库执行官方提供的db_store.sql脚本,创建global_table、branch_table、lock_table三张表。另外,每个接入全局事务的业务数据库里,必须单独执行undo_log表的建表语句,注意是每个库都要建,别只建一个。
我见过有人把db_store.sql理解成是给业务库用的,直接执行到业务库,然后TC启动后初始化报错,业务启动后一开全局事务就抛“table not exist”。这里要分清:global_table这些是TC自己的状态存储,undo_log是给RM做回滚镜像用的,两者用途完全不同。
undo_log建表语句在Seata 1.2.0的script/client/at/db目录下,每个数据库类型对应一个sql文件。MySQL版本要用带engine=innodb和varchar(64)的那份,别用旧版的。另外提醒一句,undo_log表必须跟业务表在同一个库,因为RM执行本地事务时要把回滚镜像写进undo_log,如果undo_log在别的库,本地事务提交后镜像就跟着事务一起滚掉了。
2.3 JDK版本和启动内存:容易被忽略的老版本限制
Seata 1.2.0的server包解压后,直接跑bin/seata-server.sh就行,但注意它内部带的脚本默认JVM参数是-Xmx512m。如果你们的Nacos配置中心里存的配置量比较大(Seata会用config.txt推上去一大堆key),或者事务并发量稍微上来一点,512m很容易触发Full GC甚至OOM。跑之前建议把-Xmx改到1g以上。
JDK这块,Seata 1.2.0对JDK 8的支持是完整的,但如果你用的是JDK 11以上,会遇到一个问题:Nacos 1.2.0的客户端包里有javax.annotation.Resource的引用,JDK 11开始这个包被从rt.jar里移除了,会导致Nacos客户端启动时反射报错。解决办法是给TC和所有业务服务加上javax.annotation-api的依赖,或者干脆按老项目的标准用JDK 8。
先跑TC,注册到Nacos成为nacos-service节点,再跑业务服务,这个顺序不要颠倒。TC没起来之前业务服务也能正常启动,因为Seata客户端启动时是异步注册RM的,不会因为TC不存在就启动失败,但等第一个@GlobalTransactional注解触发时,如果TC还没上线,客户端会一直重试,重试间隔还很感人。所以我的习惯是先把TC启动日志里看到“server started successfully”再启动业务服务。
3. Seata服务端的配置拆解:registry.conf和file.conf到底怎么填
3.1 registry.conf:注册中心和配置中心都指向Nacos
Seata 1.2.0服务端解压后,conf目录下有三个核心文件:registry.conf、file.conf、config.txt。其中registry.conf控制的是TC自身怎么被发现、以及TC去哪拉自己的配置。这里最容易踩的坑是把registry和config两个块混为一谈,其实前者解决“别人怎么找到我”,后者解决“我的参数从哪来”。
我的registry.conf配置如下:
registry { type = "nacos" nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" namespace = "" cluster = "default" group = "SEATA_GROUP" username = "nacos" password = "nacos" } } config { type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" } }这里有几个关键点:application这个名字,最好固定叫seata-server,因为后面客户端配置事务分组映射时会引用这个服务名;cluster默认填default,如果你后面要搞高可用集群再加;namespace建议不要填,直接走public命名空间,因为老版本Nacos对namespace的兼容有点问题,我之前填过自定义命名空间,结果TC注册进去了,但业务端通过namespace去查找时匹配不上,全局事务直接失效。
group填SEATA_GROUP是为了配合后续config.txt推送到Nacos配置中心时的分组保持一致。这里稍后要特别注意:推送配置的脚本参数里也有group,两边但凡有一个不一致,TC就拉不到自己的配置,启动时会退回到本地file.conf,造成“改配置不生效”的假象。
3.2 file.conf:store.mode选file还是db,决定了你重启会不会丢事务状态
Seata 1.2.0的file.conf文件里,最关键的一行是store.mode。我的建议是直接改成db,不要用默认的file。原因很简单:file模式把全局事务状态存在本地文件里,一旦TC重启,所有事务状态全部丢失,正在进行的分布式事务会变成悬空事务,业务数据可能已经写了一半而且无法回滚。
改成db模式后,file.conf里store节点下的配置对应关系如下:
store { mode = "db" db { datasource = "druid" dbType = "mysql" driverClassName = "com.mysql.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai" user = "seata" password = "seata123" minConn = 5 maxConn = 20 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 } }driverClassName要注意:如果你用的MySQL驱动是8.x,这里必须改成com.mysql.cj.jdbc.Driver,否则启动时报ClassNotFound。MySQL 5.7用com.mysql.jdbc.Driver没问题。url里一定要带serverTimezone参数,不然TC初始化连接池时会报时区异常。
还有一个容易漏掉的:如果store.mode配了db,但TC启动日志里出现“Failed to load configuration from registry”,然后看到file.conf里还是默认配置生效,八成是前面说的registry.conf里config.type=nacos拉取配置失败了。这时候TC会自动fallback到本地file.conf,但本地file.conf里的Store配置刚好是db,就会让人觉得配置生效了,实际却是两套配置交错,排查起来特别迷惑。
3.3 推配置到Nacos:config.txt和nacos-config.sh的使用细节
Seata 1.2.0把自身配置抽象成了config.txt,里面大概有几十个配置项,从事务分组到锁重试间隔、提交回滚线程参数都有。这些配置如果只改本地file.conf是不生效的,因为TC启动时config.type=nacos,它会优先从Nacos配置中心拉这些key,本地file.conf只是兜底。
我用的推送命令是:
cd /opt/seata/conf sh nacos-config.sh 127.0.0.1 8848这个脚本默认会把config.txt里的每一条配置用dataId形式推送到Nacos,默认分组是SEATA_GROUP。执行完后可以在Nacos控制台的配置管理里看到几十条新增配置,比如config.txt里的事务分组映射、client.tm等等。
爬坑点有三个:
第一个是脚本执行方式。nacos-config.sh依赖服务器上有可用的sh环境,Windows下用Git Bash跑没问题,但脚本里拼接curl命令时用了特殊换行符,偶尔会出现“bad interpreter”报错。解决办法是先执行dos2unix nacos-config.sh再跑,或者直接把里面curl部分复制出来逐条执行。
第二个是config.txt文件里某些配置项末尾如果带了\r\n,推送上去的dataId内容就会多一个不可见换行符,TC端反序列化时直接警告。这个我没踩过,但看到群里有人遇到过,提示一下。
第三个是如果Nacos开启了鉴权,脚本不会传username和password,推送必然失败。老版本的nacos-config.sh没有鉴权参数,需要手动改脚本或者提前在Nacos里关闭鉴权。1.2.0的Nacos默认鉴权是关闭的,所以大部分情况下直接跑即可,但如果你公司安全要求开了鉴权,这里要特殊处理。
推送完成后,最好到Nacos控制台确认一下配置数量:大概有70多条。如果只有零星的几条,说明config.txt没有被完整读取,或者脚本执行中curl请求失败被吞掉。这种情况下TC启动时一些关键配置项缺失,不会直接报错,但行为会异常,比如全局事务超时时间不对、回滚重试次数不对。
4. 业务服务端的整合配置:从依赖到数据源代理
4.1 依赖引入和版本锁定的实操写法
业务服务端整合Seata 1.2.0,我的做法是在父POM里显式声明版本,子模块全部走依赖管理,不搞隐式传递。关键依赖如下:
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.2.0</version> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>1.2.0</version> </dependency>nacos-client显式声明版本这步很关键。Seata 1.2.0依赖的nacos-client是1.1.x,而你项目里注册中心用的Nacos服务端是1.2.0,虽然1.1.x客户端连1.2.0服务端基本能用,但偶尔会遇到配置拉取时序问题。把两边对齐到1.2.0可以少很多麻烦。
同时检查项目里有没有历史遗留的dubbo和nacos-client冲突,如果有依赖树里的旧版本,用exclusion排除掉。Seata的RM要通过Nacos跟TC维持长连接,这个连接如果因为底层的nacos-client版本混乱而异常断开,是最难排查的问题——服务能起来、事务能发出去,但回滚指令总是收不到。
4.2 事务服务组映射是全局事务生效与否的分水岭
业务服务里的核心配置在application.yml里,有两条杠不能省:
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 application: name: order-service seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group registry: type: nacos nacos: server-addr: 127.0.0.1:8848 cluster: default group: SEATA_GROUP config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP service: vgroupMapping: my_test_tx_group: default这里面最绕的是tx-service-group和vgroupMapping的关系。tx-service-group是业务服务自己定义的一个逻辑分组名,它需要通过vgroupMapping映射到TC的集群名。Seata 1.2.0的套路是:业务端告诉TC“我属于my_test_tx_group这个事务组”,TC根据这个事务组名去Nacos上找名为default的集群里注册的seata-server实例。
两个问题特别常见:一个是vgroupMapping没配,或者配置了但value写成了default集群里不存在的名字,导致客户端拿到“no available service”的错误;另一个是config.txt推送上去的配置里也有一个transaction.service.group.mapping.my_test_tx_group的配置,跟application.yml里的vgroupMapping存在重复定义。客户端读取时以哪个为准,取决于config.type的优先级。实测结果是application.yml里的seata.service.vgroupMapping会覆盖Nacos配置中心的同名配置,所以两边最好保持一致,否则会出现配置不一致导致的间歇性找不到TC。
我把vgroupMapping配置和config.txt里对应的key做了同步:config.txt里其实不需要专门配transaction.service.group.mapping,因为客户端自己会读application.yml。但如果配置中心里恰好有这条key且指向错误集群,就会覆盖application.yml的配置。排查这类问题最快的办法是打开Nacos配置列表,搜一下有没有跟事务组相关的key。
4.3 数据源代理和undo_log的关联逻辑
Seata AT模式的核心机制是数据源代理加undo_log快照。业务服务里千万不要自己new DruidDataSource扔给MyBatis管理,要让Seata代理掉。用seata-spring-boot-starter时它会自动处理DataSourceProxy,前提是你没有手动排除DataSourceAutoConfiguration。
如果项目里自己定义了数据源Bean,要把Seata的代理包装起来,这是很多组件化项目的硬伤。正确写法类似:
@Bean public DataSource dataSource(DataSource druidDataSource) { return new io.seata.rm.datasource.DataSourceProxy(druidDataSource); }这个代理的用途是:RM在执行业务SQL前,会先生成before image;执行SQL后,再生成after image;这两张镜像连同SQL类型、表名、主键值一起组织成undo_log写入当前事务的本地库。如果全局事务后来回滚,RM通过undo_log里的before image逆向补偿数据。如果全局事务提交,RM会在分支事务提交阶段把undo_log相应记录删除。
有两点要注意:第一,事务方法里用到的所有数据源里都要有undo_log表,包括读操作和写操作走同一个数据源的情况;第二,业务SQL不要用MyBatis自带的批量更新插件直接怼,因为底层生成的SQL可能带limit,Seata对limit子句的解析在1.2.0版本里存在已知边界问题。如果确实要用批量操作,建议拆成单条SQL循环,或者明确记录影响行数,不要图快。
5. 端到端跑通一个分布式事务需要验证什么
5.1 最小可验证案例:跨库扣减为什么能回滚
我建议第一次跑通别直接改业务代码,单独做一个最小案例:一个订单服务在自己的order库写一条订单记录,同时调用一个库存服务在stock库里扣减库存。两边都加上@GlobalTransactional。然后故意让库存服务抛个RuntimeException,看订单库的数据是否回滚。
这个案例能跑通,说明五件事是好的:TC注册进Nacos、TM能连上TC、全局事务XID能透传、RM能注册分支事务、undo_log回滚链路正常。
透传这一环最容易被忽略。这里用的案例里我推荐最稳妥的透传方式:在库存服务的接口参数里显式带XID参数,业务代码里手动调用RootContext.bind(xid),而且库存服务方法里再包一层try-finally,确保xid在方法结束时unbind。老版本对线程池里透传XID的支持很弱,网上有文章说配了spring.sleuth或者自定义线程池装饰器就能自动透传,我实测1.2.0不行,老老实实手工透传最可靠。
5.2 验证过程中如何看日志判断走到哪一步
跑完案例后,按顺序看三个日志位置:
一是TC日志,在seata-server的logs目录下有个seata-tc.log。全局事务开启时应该出现“Begin new global transaction”,分支事务注册时出现“Register branch successfully”。如果只看到Begin没有Register,问题出在TM到RM之间的XID透传上;如果Register都没有,说明RM根本没连上TC。
二是订单服务日志,出现“Global transaction commit”或“Global transaction rollback”字样。如果rollback了但库存服务对应的数据没变回去,去库存库查undo_log表,看看镜像记录状态字段是1(待回滚)还是0(已回滚)。状态为1说明回滚指令没执行到或执行失败。
三是库存服务日志,看有没有“UndoLogManager rollback”关键字。正常回滚时RM会打印一条类似“branch rollback success”的日志。如果这个日志出现但数据没变,检查before image的解析,常见坑是业务表没有主键,Seata无法定位数据行,回滚SQL全部失效但日志完全正常。
5.3 从1.0/1.1升到1.2.0的平滑路径回顾
如果你们项目之前用的Seata是老版本,升级到1.2.0时注意三点。
第一,配置项name在1.1到1.2之间有个别调整,比如service.vgroupMapping早期无seata前缀,1.2.0必须要带seata前缀。这个细节容易让人在升级后以为配置失效。
第二,老版本undo_log表结构字段可能没有undo_id的默认值,直接从老表改新表容易遇到主键冲突。最保险的做法是drop掉老表重新执行建表脚本,历史镜像清理掉,因为1.2.0的回滚解析器和1.0的回滚格式在字段语义上不完全兼容,保留旧镜像可能导致回滚时类型转换异常。
第三,服务端如果是从1.1升上来的,global_table里的branch_id和transaction_id字段有长度变化,最好直接清空三张状态表,让TC重新分配。业务并发不高的话,这步操作几分钟就完成了,不用停机太久。
6. 从本次爬坑中总结的排错优先级与常用命令
6.1 遇到问题的排查顺序别搞反
我踩完这些坑后总结了一套排查顺序,从高到低大致是:
- 版本是否对齐。先看服务端和客户端Seata版本、nacos-client版本,这是最容易被忽略但影响最大的。
- 配置是否重复定义。检查application.yml与Nacos配置中心里同名配置是否冲突。
- 注册是否成功。Nacos控制台服务列表里有没有seata-server和业务服务。
- 连的是哪个TC。看业务端日志输出的TC地址,跟Nacos上注册的TC实例是否一致。
- 事务分组映射是否落到正确集群。这一步错了,通常报no available service。
按这个顺序排查,能在十分钟内定位大部分问题。反过来先看SQL、先看事务注解,经常白忙活。
6.2 常用验证命令和脚本
下面这几个命令我每次整合Seata都要用到,列出来给大家参考:
# 查看TC是否注册上Nacos curl -X GET "http://127.0.0.1:8848/nacos/v1/ns/service/list?serviceName=seata-server" # 查看配置中心里是否推入了Seata配置(dataId前缀) curl -X GET "http://127.0.0.1:8848/nacos/v1/cs/configs?search=accurate&dataId=&group=SEATA_GROUP&pageNo=1&pageSize=100" # 实时跟TC日志 tail -f /opt/seata/logs/seata-tc.log # 跟业务端Seata相关日志(按service名调整路径) find /logs -name "*seata*" | xargs tail -f另外有一个隐藏技巧:Seata 1.2.0业务端日志的trace里会打印RPC连接的IP和端口,正常连上TC时日志里有“NacosServiceRegistry register success”字样,这个日志如果能看到且TC地址正确,说明注册没问题。如果trace里没有任何TC地址相关内容,直接怀疑客户端和服务端版本不对齐。
6.3 高可用部署框架下的额外补充配置
如果你们的Nacos是集群而不是单机,registry.conf里的serverAddr要写多个地址,用英文逗号分隔,类似“ip1:8848,ip2:8848,ip3:8848”。nacos-config.sh脚本推送配置时同理,如果是集群,建议指定leader地址或全部地址,脚本默认只接收一个serverAddr参数,但新版nacos-config.sh支持把多个地址作为字符串传入。
Seata TC本身的高可用,在1.2.0里靠的是Nacos注册中心多实例加DB共享状态。两台服务器分别部署TC,都配置同一套MySQL作为store.db,注册时都用同一个application名和集群名default。Nacos会自动做负载均衡,客户端连上一台TC,如果这台TC挂了,下次连接会切换到另一台。这里我提醒一个坑:如果两台TC的store.db配置的是不同库,那高可用等于没有。同一个事务的中途提交和回滚,必须落在同一份数据库状态上。
7. 我现在维护这套组合的一些个人体会
这套Nacos 1.2.0加Seata 1.2.0的组合,我能跑通,靠的其实是踩坑踩出来的肌肉记忆。最开始那几天我反反复复在Nacos控制台和TC日志之间切换,一度怀疑是防火墙挡了端口。后来把三个角色(TC、TM、RM)各自的日志按时间线拼在一起看,才意识到问题出在事务分组映射上,跟网络一毛钱关系没有。
有一点是经验之谈:老版本组件出问题,不要第一时间怀疑代码逻辑,先把配置链路打通。Seata 1.2.0的配置链路在正常状态下是“本地file.conf兜底 + registry.conf指向Nacos + Nacos配置中心持有权威配置”,任何一环掉链子,都会表现为“事务注解加了但不生效”。把这段链路理清楚,后续维护就顺了。
最后分享一个我后来一直沿用的习惯:每次改了Seata相关配置,不管是TC端的file.conf还是业务端的application.yml,我都习惯先把TC重启,再到Nacos控制台刷新配置对比一下。因为Seata 1.2.0的配置是启动时一次性拉取的,不是动态监听,改了配置不重启,它永远不会知道。明白了这一点,很多“改了没生效”的奇怪现象都能解释得通了。