最近在一个微服务拆分改造项目里我撞上了分布式事务这道坎。用户下单这个操作牵扯订单服务、库存服务、余额服务任何一个成功而另一个失败账就对不上。考虑团队当时已经在用 Nacos 1.2.0 做注册中心和配置中心顺势把 Seata 1.2.0 也接进来让注册发现、配置管理、分布式事务三件事全部走同一套基础设施是成本最低的路。折腾了几天从服务端启动到客户端事务生效中间踩了无数个配置坑这篇就把 Nacos 1.2.0 整合 Seata 1.2.0 的完整过程和爬坑实录写出来给正在搞 Seata 的同行做个参照。1. 整合前必须想清楚的事1.1 为什么要用 Seata而不是自己写事务微服务架构下每个服务各管一个数据库本地事务天然无法跨库。你不可能靠删库重跑来弥补数据不一致。Seata 做的事并不神秘它把一次业务操作拆成两阶段提交第一阶段各分支服务先执行业务 SQL 并且记录回滚日志第二阶段由 Seata Server 统一决定提交或回滚。业务代码几乎无侵入只需要在入口方法上加一个 GlobalTransactional 注解这对已经写完的接口来说改动量很小。1.2.0 版本是 Seata 比较稳定的一代文档齐全社区讨论多问题排查资料也容易搜到。Nacos 1.2.0 则是一个经典版本很多公司的 Spring Cloud Alibaba 体系都停在它上面。两个版本配套使用的例子很多网上踩坑记录也多真出问题不会孤立无援。1.2 先分清 Seata 里的三个角色很多人在配置阶段就把 TM、RM、TC 搞混了后面排查问题会非常痛苦。TCTransaction Coordinator事务协调者也就是 Seata Server负责全局事务的开启、提交和回滚。TMTransaction Manager事务管理器业务服务里加了 GlobalTransactional 注解的就是 TM它告诉 TC 我开了个全局事务。RMResource Manager资源管理器业务服务里被代理的数据源就是 RM它执行本地事务并且向 TC 注册分支事务。整个调用链上的每个业务服务既是 TM 的调用方也可能同时是 RM。Seata Server 本身不参与业务它只负责接收 TM 的开始、RM 的分支注册和最后的决议通知。理解这个关系之后你就知道配置时哪些东西要给服务端、哪些要给客户端。1.3 版本兼容性和依赖风险在动手之前我建议先把你当前项目的 Spring Boot、Spring Cloud、Spring Cloud Alibaba 版本列出来。Seata 1.2.0 的 seata-spring-boot-starter 官方适配 Boot 2.2.x 到 2.3.x 这个区间如果你项目在 Boot 2.4 以上自动装配很可能出问题。我见过有人在 Boot 2.6 里硬上 Seata 1.2.0结果配置项绑定不上服务起不来。另外 Seata 1.2.0 依赖库比较老netty、kotlin、protobuf、guava 都和现在主流版本有偏差。引入之前建议先跑一下 mvn dependency:tree 看清楚依赖冲突不要等问题出现了再去补排除。2. 服务端配置实操2.1 先把 Nacos 1.2.0 准备到位如果你的 Nacos 还是默认安装、默认配置那基本可以直接长这个样子启动cd nacos/bin sh startup.sh -m standaloneNacos 1.2.0 默认会把配置数据存在内嵌的 Derby 里单机演示没问题但团队开发时重启一次配置就丢非常坑。所以我强烈建议把 Nacos 的存储切到 MySQL改 conf/application.propertiesspring.datasource.platformmysql db.num1 db.url.0jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncodingutf8connectTimeout3000socketTimeout3000autoReconnecttrueuseUnicodetrue db.userroot db.passwordroot改完之后先把 conf 目录下自带的 nacos-mysql.sql 导入到 nacos_config 这个库里然后再启动 Nacos。这一步很多人忽略结果 Nacos 控制台打开配置列表是空的还以为刷出来了实际数据写到了别处。2.2 Seata Server 的 registry.conf 怎么填Seata Server 解压之后conf 目录下有两个核心文件registry.conf 和 file.conf。先说 registry.conf它管的是 Seata Server 自己如何注册到 Nacos、以及从 Nacos 拉配置。完整内容填成registry { type nacos nacos { application seata-server serverAddr localhost:8848 namespace cluster default username password } } config { type nacos nacos { serverAddr localhost:8848 namespace group SEATA_GROUP username password } }注意几个关键点application 这个值默认是 seata-server客户端发现服务时会找服务名为 seata-server 的实例别乱改。cluster 默认是 default客户端事务分组映射最终也会指向 default两者必须对上。namespace 留空代表使用 Nacos 的 public 命名空间。如果你项目用了自定义命名空间这里必须填和客户端一致的 namespace ID否则两边各认各的服务能被 Nacos 看到但客户端就是连不上。2.3 事务存储改成 db 模式Seata Server 默认用 file 模式保存全局事务会话简单但不利于生产。既然有 Nacos建议直接用 db 模式在 file.conf 中把 store 配置改成store { mode db db { datasource druid dbType mysql driverClassName com.mysql.cj.jdbc.Driver url jdbc:mysql://127.0.0.1:3306/seata?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai user root password root minConn 5 maxConn 30 globalTable global_table branchTable branch_table lockTable lock_table queryLimit 100 maxWait 5000 } }同时你得先建好 seata 库并执行 Seata 发行包 conf 目录里的 db_store.sql 脚本它会创建 global_table、branch_table、lock_table 三张表。我建议自己把这三张表手动建一遍不要依赖脚本报错之后才看毕竟 MySQL 版本和 sql_mode 差异可能导致脚本执行一半就中断。提示db 模式下Seata Server 一旦在 Nacos 配置中心读到了 seata.properties会优先使用配置中心里的 store 配置本地的 file.conf 反而成为兜底。2.4 在 Nacos 上发布 seata.propertiesSeata 1.2.0 合并了配置项会把所有相关配置都放到同一个 dataId 里。建议在 Nacos 控制台的 public 命名空间下创建一个 dataId 为 seata.properties、分组为 SEATA_GROUP 的配置。内容参考service.vgroupMapping.my_test_tx_groupdefault store.modedb store.db.datasourcedruid store.db.dbTypemysql store.db.driverClassNamecom.mysql.cj.jdbc.Driver store.db.urljdbc:mysql://127.0.0.1:3306/seata?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai store.db.userroot store.db.passwordroot store.db.minConn5 store.db.maxConn20 store.db.globalTableglobal_table store.db.branchTablebranch_table store.db.lockTablelock_table store.db.queryLimit100 store.db.maxWait5000这里就出现第一个坑本地 file.conf 里事务组映射用的是service.vgroup_mapping.my_test_tx_group带下划线Nacos 配置中心用的是service.vgroupMapping.my_test_tx_group驼峰。大小写和下划线不一致是 Seata 1.2.0 最容易踩的雷。我建议以配置中心这份为准本地 file.conf 保持原样作为兜底但不要两边配置不同值。2.5 启动 Seata Server 并验证注册状态配置都改好之后直接执行启动脚本cd seata/bin sh seata-server.sh -p 8091 -m db-m db 参数最好显式带一下避免某些版本按 file 模式启动。启动完成后去 Nacos 控制台的服务列表里看应该能看到一个名为 seata-server 的服务下面有一个集群名为 DEFAULT 的实例。能看见这一步服务端就算通了。如果看不到实例优先看 seata-server 的日志常见原因包括 Nacos 地址写错、namespace 填了不存在的 ID、registry.conf 配置没生效。我有一次就是 namespace 填了自定义空间 IDNacos 服务列表里切到该空间能看到 seata-server但客户端在 public 空间找自然找不到。3. 客户端接入配置3.1 依赖引入与冲突排除客户端业务服务需要引入 Seata 的 Spring Boot Starter。以 Maven 为例dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.2.0/version exclusions exclusion groupIdio.netty/groupId artifactIdnetty-all/artifactId /exclusion exclusion groupIdorg.jetbrains.kotlin/groupId artifactIdkotlin-stdlib/artifactId /exclusion /exclusions /dependency为什么排除 netty-all因为 Spring Cloud 全家桶里各个组件用的 netty 版本经常不一致Seata 1.2.0 依赖的 netty 版本比较老很容易把项目原有的 netty 版本覆盖掉导致运行时出现 ChannelOption、HashedWheelTimer 这类 NoClassDefFoundError。排除之后用项目现有的 netty 版本即可。kotlin-stdlib 也是类似情况Seata 内部用了 kotlin 运行时的功能如果项目主版本太低就会 NoSuchMethodError。3.2 不需要客户端再放 registry.confSeata 1.2.0 的 Spring Boot Starter 支持直接在 application.yml 里写客户端配置比在业务服务里塞一个 registry.conf 更干净。我推荐这种方式避免多个微服务各自复制文件、改漏一个就出了问题。参考配置如下spring: application: name: order-service cloud: nacos: discovery: server-addr: localhost:8848 seata: enabled: true application-id: ${spring.application.name} tx-service-group: my_test_tx_group enable-auto-data-source-proxy: true registry: type: nacos nacos: server-addr: localhost:8848 namespace: cluster: default config: type: nacos nacos: server-addr: localhost:8848 namespace: group: SEATA_GROUP service: vgroup-mapping: my_test_tx_group: default disable-global-transaction: false这里 tx-service-group 和 service.vgroup-mapping 里的 key 必须一致。它的作用机制是业务服务这个 TM 告诉 TC我属于 my_test_tx_group 这个事务分组TC 根据 vgroupMapping 找到对应的后端集群名 default然后去 Nacos 注册中心里找 cluster 为 default 的 seata-server 实例。这个链条一环不匹配就会报找不到服务。3.3 业务代码接入事务客户端配置完之后业务代码改动非常小。在需要进行全局事务的入口方法上加上注解GlobalTransactional public void createOrder(OrderDTO orderDTO) { orderMapper.insert(orderDTO); stockClient.deductStock(orderDTO.getProductId()); balanceClient.deductBalance(orderDTO.getUserId()); }注意这个入口方法的调用方必须是另一个 Bean 或者外部接口不能是同一个类内部方法调用。因为 GlobalTransactional 是基于 Spring AOP 代理的类内部 this 调用不会经过代理事务自然不会生效。我在实际项目中遇到过一次把 createOrder 放在了 OrderService 里由 OrderController 调用没问题后来重构的时候在同一个类里新增了一个方法内部去调 createOrder事务就完全没生效排查了很久才反应过来。3.4 客户端需要准备 undo_log 表AT 模式的回滚依赖业务数据库里的 undo_log 表。每个参与全局事务的业务库都要建不能只建在某个库里。建表 SQL 建议用 Seata 发行包里的 db_undo_log.sql这里也贴一个标准版本CREATE TABLE IF NOT EXISTS undo_log ( id BIGINT NOT NULL AUTO_INCREMENT, branch_id BIGINT NOT NULL, xid VARCHAR(100) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info LONGBLOB NOT NULL, log_status INT NOT NULL, log_created DATETIME NOT NULL, log_modified DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid, branch_id) ) ENGINE InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET utf8mb4;如果业务库没有 undo_log 表事务执行阶段是正常的但一旦需要回滚Seata 会尝试往 undo_log 表写入回滚记录直接报 Table xxx.undo_log doesnt exist。这个错误在测试环境回滚场景下才会暴露很隐蔽。4. 爬坑过程实录4.1 服务端能注册客户端连不上我把 Seata Server 启动后Nacos 服务列表里已经能看到 seata-server 实例了但业务服务一启动就报can not connect to services-server。排查过程先确认业务服务所在的机器能不能 telnet 到 Seata Server 的 8091 端口网络不通就别往后查了。再确认 Seata Server 的 registry.conf 里 namespace 和 cluster 是否和客户端一致。这是高频坑服务端注册到了 namespace A客户端配置里 namespace 留空两边各看各的。最后看 Seata Server 日志确认客户端请求有没有到达服务端如果连日志都没有肯定是网络或注册发现问题。注意Seata Server 的 registry.conf 里 nacos.application 不要随便改成业务名客户端从 Nacos 拿到的服务名必须是 seata-server改了名字客户端就找不到了。4.2 提示 No available service in cluster default这个报错在客户端日志里出现时说明客户端已经找到了注册中心但没找到符合条件的 Seata Server 实例。常见原因两种Seata Server 注册时 cluster 值不是 default。检查服务端 registry.conf和客户端 service.vgroup-mapping 里映射的值是否一致。Seata Server 注册到了别的 namespace客户端却用 public 空间去查。这种问题在 Nacos 控制台切到对应 namespace 一眼就能看出来。我在测试环境里遇到过一次因为服务端 registry.conf 里 cluster 被之前的同事改成了 SEATA_GROUP而客户端 vgroupMapping 指向 default结果两边互相找不到。记住cluster 是集群名和配置中心的分组 SEATA_GROUP 完全是两个东西不要混在一起。4.3 配置中心读不到 seata.properties客户端配置 config.type 指向 Nacos 后它会去拿 dataId 为 seata.properties、group 为 SEATA_GROUP 的配置。如果你只改了 application.yml 里的 seata.config.nacos.group但 Nacos 上其实没创建这个配置客户端会退回到本地 file.conf 读取。这时候最容易出现的情况是本地 file.conf 里的 store 配置和服务端不一致客户端以为自己在 file 模式下工作。建议的做法是在 Nacos 控制台明确创建 seata.properties然后重启 Seata Server 和业务服务再看 registry 模式是否真正切换成 Nacos。如果不想用配置中心也可以把 config.type 改成 file让业务服务只在本地 file.conf 读取哪种方式都不推荐混着来。4.4 本地 file.conf 和配置中心命名差异Seata 1.2.0 这个版本在配置项命名上有点历史遗留本地 file.conf 的配置 key 是下划线风格service.vgroup_mapping.my_test_tx_group default而 Nacos 配置中心里同一个含义的 key 是驼峰service.vgroupMapping.my_test_tx_groupdefault如果你在本地 file.conf 和配置中心都配置了事务分组但值不一样客户端会以配置中心为准本地配置被覆盖。反过来说如果配置中心没配置客户端读本地 file.conf 的 vgroup_mapping。我在一个项目里看到有人把本地 file.conf 里改成驼峰启动时直接报非法配置项这个别折腾文件里该用什么格式就用什么格式。4.5 依赖冲突导致服务启动失败Seata 1.2.0 引入后我的某个服务启动时直接抛出了NoSuchMethodError: kotlin.collections.ArraysKt.copyInto。查下来是 Seata 内部依赖 kotlin-stdlib 版本比较高而项目主 pom 里锁了一个老版本。把 kotlin-stdlib 排除掉让 Spring Boot 依赖管理统一版本之后就好了。还有一个常见的是 netty 冲突报错类型五花八门。最直接的办法是在引入 Seata 时先排除 netty-all 和 kotlin-stdlib让项目主导版本。如果项目里用了同版本的 netty业务代码不受影响如果使用的是 Spring Cloud Gateway 这类重度依赖 netty 的组件尤其要注意。4.6 事务名称过长或参数类型问题在数据库里如果看到事务回滚失败或者 Seata 日志里出现Could not found any open transaction不一定配置的问题还可能是表字段和数据类型不匹配。比如 MySQL 8 的 JDBC 驱动要求 url 里带 serverTimezone否则时间字段解析会报错连带 undo_log 的写入失败。我踩过最恶心的一次是业务表主键用了 varchar 但存的是数字字符串Seata 的全局锁解析主键时类型转换出错。遇到这类问题先看 Seata 所在服务端的完整异常堆栈别盯着客户端日志猜。4.7 多数据源场景下的事务边界如果业务服务里配置了多个数据源Seata 默认只会代理主数据源其他数据源参与的操作不会被纳入全局事务。项目里有一个服务同时连主库、订单库、报表库结果报表库的操作回滚不了。解决办法是用 Seata 手动包装数据源Bean Primary public DataSource seataDataSource(DataSource dataSource) { return new DataSourceProxy(dataSource); }对于不需要纳入全局事务的数据源保持原样即可。这么一改需要事务的库走 Seata 代理不需要的库不参与边界清晰很多。4.8 全局锁等待超时高并发测试的时候全局事务之间会抢同一行数据的锁如果前一个事务还没提交后一个事务会一直重试拿锁。默认锁重试间隔和次数在压测时不够会出现Lock wait timeout的异常。我在 application.yml 里调大了参数seata: client: rm: lock: retry-interval: 10 retry-times: 30retry-interval 单位是毫秒调大到 10 毫秒重试 30 次基本能满足测试环境的并发要求。生产环境要根据实际响应耗时来压测调整不要盲目加太大否则请求排队会拖垮接口。4.9 Nacos 重启后 Seata 连接不上Nacos 1.2.0 本身是单机能力有限的如果 Nacos 重启期间 Seata Server 和业务服务都还活着它们和 Nacos 的长连接会断开。Nacos 恢复后Seata Server 大概率能自己重新注册但客户端不一定能及时感知。最省心的处理办法Nacos 恢复后按顺序重启 Seata Server再重启业务服务。不要在生产环境随意重启 Nacos至少要把这个操作排进灰度发布流程里。5. 常见问题速查表现象大概率原因处理方式服务端不能注册到 Nacosregistry.conf 地址、namespace 错误核对 serverAddr、namespace看 seata-server 日志客户端报 can not connect网络不通或服务端未启动检查 8091 端口连通性客户端报 No available servicecluster 或 namespace 不一致对齐服务端 cluster 和客户端 vgroupMapping配置中心读不到配置dataId/group 写错或未创建在 Nacos 上创建 seata.properties事务没回滚GlobalTransactional 没生效检查注解位置、同类内部调用、数据源代理回滚时报 undo_log 表不存在业务库没建 undo_log每个业务库执行 undo_log 建表脚本启动报 NoSuchMethodErrorkotlin/netty 等依赖冲突排除冲突依赖统一版本全局锁超时并发下重试参数不足调大 lock.retry-interval 和 retry-timesMySQL 时间字段报错JDBC url 缺 serverTimezone增加 serverTimezoneAsia/Shanghai6. 最后的实操心得这套 Nacos 1.2.0 和 Seata 1.2.0 的组合到现在为止我维护过好几套环境整体稳定性没让我失望但配置环节确实容易反复踩坑。个人体会是配置这个东西每改一个变量就要重启验证一次不要一次改一堆不然出了问题根本不知道是哪一项影响的。我习惯的顺序是 Nacos 先起来Nacos 控制台能看到配置再起 Seata ServerNacos 能看到 seata-server 实例最后起业务服务日志里出现 Global Transaction 相关的注册信息再开始测分布式事务。另外如果你是第一次接 Seata别急着直接改造真实业务。搭一个最小闭环订单服务调库存服务库存服务里故意抛异常验证全局回滚是否生效。这个 Demo 环境我保留了很久每次换个新环境、升个版本都先用它跑一遍确认没问题再放业务进去。分布式事务这种功能上线后出问题的代价远大于前期准备的时间值得多花半小时把链路完全打通。