资讯动态

Spring Cloud Alibaba+Seata+Nacos分布式事务搭建实践

发布时间:2026/10/15 9:27:41 来源:尧图企业网站定制
第一次要把下单、扣库存、扣余额拆到三个库里时我还觉得问题不大本地Transactional一把锁搞定。后来才意识到Transactional只能保证一个数据源内的一致性三个库就像三张桌子你没法用一只手同时按住三张桌子上的纸。Spring Cloud Alibaba 2.2.0 Seata 1.2 Nacos 1.2.1 这组搭配就是我最终选定的解法。如果你正在搜“Spring Cloud Alibaba 2.2.0 Seata 1.2 Nacos 1.2.1 搭建分布式事务”大概率已经踩到或即将踩到版本坑、配置坑以及“注解我加了但事务没生效”的坑。这篇把从 Server 部署到客户端接入、再到线上排障的经验一次写清适合要落地 Seata AT 模式的团队参考。顺便纠正一个措辞这里说的是“事务”Transaction不是“事物”但我知道很多人搜的时候都会打成“事物”。这不影响搜索引擎召回倒是能看出大家都被分布式事务坑得不轻。1. 选型与版本约束为什么是 Seata 1.2 而不是别的方案1.1 分布式事务为什么不能靠本地事务硬扛服务拆分之前订单、库存、账户都在同一个数据库里一个事务就能保证要么全部成功、要么全部回滚。服务拆开之后下单要调三个服务每个服务写各自的库这时候任何一个服务成功、另一个服务失败账就平不了。本地事务解决不了跨库问题本质原因是本地事务的边界被限制在单一数据库连接里。你不可能在一个Transactional方法里让两个不同数据源的事务共享同一个提交或回滚决策除非引入一个独立的协调者。这个协调者就是分布式事务框架做的事。1.2 主流方案对比我最初纠结过几种方案本地消息表、事务消息、TCC、直接上 2PC/XA。它们的取舍差别很大。方案侵入性一致性适用场景本地消息表中最终一致异步、能接受延迟事务消息中最终一致依赖消息中间件TCC高强一致资金类、需要预留资源2PC/XA低强一致数据库支持有限性能损耗明显Seata AT低最终一致 全局锁大多数中小型分布式事务Seata 的 AT 模式对我最有吸引力的一点是业务代码几乎不用改只需加一个GlobalTransactional注解Seata 在底层拦截 SQL、记录快照、自动补偿。前提是 SQL 能被解析成标准 INSERT/UPDATE/DELETE如果你的业务大量使用存储过程、复杂嵌套 SQL就要慎重评估。1.3 版本兼容性硬约束版本组合是这套实践里最容易被忽略的环节。组件推荐版本备注Spring Boot2.2.5.RELEASESCA 2.2.0 基于 2.2.x 验证Spring CloudHoxton.SR12020 之后配置方式不同Spring Cloud Alibaba2.2.0.RELEASE标题固定写法Seata1.2.0必须手动覆盖默认版本Nacos1.2.1同时承担注册中心与配置中心MySQL5.7 / 8.08.0 注意驱动与时区参数这里有个大坑Spring Cloud Alibaba 2.2.0.RELEASE 的 BOM 会把 Seata 相关组件钉在 1.0.0和 Server 端的 1.2 不一致。客户端注册分支时会出现版本不兼容的异常或者明明配置都对了事务就是不生效。所以后面 Maven 部分我会专门给出强制覆盖版本的做法。2. Seata Server 1.2 在 Nacos 1.2.1 上的部署细节2.1 下载与启动前要分清两个配置文件先下载seata-server-1.2.0.tar.gz解压后conf目录下有两个文件是核心registry.conf和file.conf。registry.conf管的是“Seata Server 注册到哪里、配置从哪里读”file.conf管的是“事务存储方式、事务分组、锁超时等运行参数”。很多人只改了registry.conf却忘记file.conf里的事务分组要和客户端一致导致客户端报找不到服务。2.2 registry.conf 改成 Nacos用 Nacos 1.2.1 同时做注册中心和配置中心时registry.conf这样改registry { type nacos nacos { serverAddr 127.0.0.1:8848 namespace cluster default } } config { type nacos nacos { serverAddr 127.0.0.1:8848 namespace group SEATA_GROUP } }注意namespace留空代表使用 Nacos 的public命名空间不要随手填一个不存在的 ID。cluster默认是default这个值必须和客户端里 Seata 的 cluster 一致否则服务注册了也发现不了。2.3 存储模式先用 file 跑通再切 dbSeata 1.2 Server 支持file和db两种存储模式。单机快速验证用file最省事但生产环境我会建议用db原因是 file 模式下全局事务记录存在本地文件里集群部署时各节点状态不共享。如果切到 db 模式需要先建一个seata库然后执行 Seata 压缩包自带的db_store.sql。里面有三张核心表global_table存全局事务branch_table存分支事务lock_table存全局锁。MySQL 8 用户还要注意把驱动换成com.mysql.cj.jdbc.Driver连接串加上serverTimezoneAsia/Shanghai否则启动必报时区错误。2.4 事务分组写入 Nacos 配置中心Seata 有一个“事务分组”的概念。客户端配置tx-service-groupmy_test_tx_groupServer 端必须有一条映射关系告诉 Seata这个分组对应哪个集群通常是service.vgroupMapping.my_test_tx_groupdefault。我用的是把配置导入 Nacos 的方式。解压包conf目录下有nacos-config.txt和nacos-config.sh脚本会把nacos-config.txt里的配置逐条写入 Nacos默认 group 是SEATA_GROUP。在导入之前确认nacos-config.txt里有这一行service.vgroupMapping.my_test_tx_groupdefault如果初始文件里没有就手动追加或者在 Nacos 控制台直接新建一条 DataId 同名的配置。这一步漏掉客户端会在注册分支时直接报No available service default found。2.5 启动并验证注册结果配置改好后启动命令很简单sh seata-server.sh -h 127.0.0.1 -p 8091看到日志输出Server started只算成功了一半。还要去 Nacos 控制台的“服务管理-服务列表”确认能看到seata-server这个服务且集群名为default。如果服务列表里没有优先检查 registry.conf 里的serverAddr、namespace和 cluster。注册中心通了才算真正跑起来。3. 客户端接入Spring Cloud Alibaba 2.2.0 应用里的每一步改动3.1 Maven 依赖与版本覆盖客户端接入第一步是让三个业务服务都引入 Nacos 和 Seata 相关依赖。我建议在dependencyManagement里同时管理 Spring Cloud Alibaba 和 Seata 版本dependencyManagement dependencies dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version2.2.0.RELEASE/version typepom/type scopeimport/scope /dependency dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.2.0/version /dependency /dependencies /dependencyManagement实际依赖这样写dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-seata/artifactId /dependencyspring-cloud-starter-alibaba-seata会传递引入 Seata 1.0.0我在依赖管理里强制覆盖成 1.2.0。改完后运行mvn dependency:tree -Dincludesio.seata:*如果还看到 1.0.0说明覆盖没生效要检查依赖管理位置是否正确或者在spring-cloud-starter-alibaba-seata的依赖里显式排除旧版本。3.2 bootstrap.yml 配置Nacos Config 需要在应用程序早期加载所以放在bootstrap.yml里。每个服务大同小异以order-service为例spring: application: name: order-service cloud: nacos: config: server-addr: 127.0.0.1:8848 file-extension: yaml group: DEFAULT_GROUP discovery: server-addr: 127.0.0.1:8848 namespace: public alibaba: seata: tx-service-group: my_test_tx_group服务名决定了 Nacos 配置中心的 DataId比如order-service.yaml。spring.cloud.alibaba.seata.tx-service-group是 Seata 客户端的事务分组名必须和 Server 端 vgroupMapping 的前半段一致。3.3 手工数据源代理最容易被忽略的一步Seata AT 模式要拦截 SQL、生成 undo_log前提是数据源必须经过DataSourceProxy包装。只写业务代码但不代理数据源事务会静默失效这是最常见的“注解没用”原因。我会在每个参与分布式事务的服务里手动声明一个DataSourceProxyConfiguration public class DataSourceConfig { Bean Primary public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } }为什么手动做而不是完全依赖自动装配因为 Seata 的自动代理在多数据源、动态数据源场景下经常失效而且一旦失效没有任何启动报错。手动声明之后容器里已经有DataSourceProxy自动配置会主动让路不会重复代理。3.4 每个业务库都要建 undo_log 表AT 模式的回滚依赖 undo_log 表。这张表不是建在 Seata Server 的库里而是建在每一个参与分布式事务的业务库里也就是order库、storage库、account库都要建。CREATE TABLE undo_log ( id bigint(20) NOT NULL AUTO_INCREMENT, branch_id bigint(20) NOT NULL, xid varchar(100) NOT NULL, context varchar(128) NOT NULL, rollback_info longblob NOT NULL, log_status int(11) NOT NULL, log_created datetime NOT NULL, log_modified datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid, branch_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8;有同事问过我只建在核心库行不行答案是大概率不行。任何分支事务回滚时都要查 undo_log缺表的分支会直接回滚失败。3.5 客户端 registry.conf 与 file.confSeata 1.2 的客户端 starter 默认会读 classpath 下的registry.conf和file.conf。最简单的做法是从 Seata Server 解压包 conf 目录复制一份把注册和配置来源都改成 Nacoscluster 保持default。registry { type nacos nacos { serverAddr 127.0.0.1:8848 namespace cluster default } } config { type nacos nacos { serverAddr 127.0.0.1:8848 namespace group SEATA_GROUP } }这里要注意 group 和 Server 端一致都是SEATA_GROUP。如果客户端读的 group 不对拉不到配置事务分组映射就失效表现依然是找不到服务。4. AT 模式是怎么协调三个库的全局锁和 undo_log 的配合4.1 第一阶段本地提交但把后路留好当一个请求进入order-service的GlobalTransactional方法时Seata 会先向 Server 申请一个全局事务拿到 XID然后把它绑定到当前线程。接下来每个服务执行本地 SQL看起来和普通事务一样UPDATE t_storage SET count count - 1 WHERE id 1;但 Seata 的DataSourceProxy在底层做了两件事把修改前的主键值、字段值记为“前镜像”把修改后的值记为“后镜像”并且以二进制形式写入undo_log。写完 undo_log 后再提交本地事务。我一开始觉得这个设计很奇怪明明分支还没全部执行完本地事务怎么就提交了原因在于 Seata 需要先释放数据库连接资源避免长事务占用连接。如果后续分支失败走的是补偿回滚而不是像本地事务那样直接回滚。4.2 第二阶段提交是清理回滚是补偿全局事务结束时Server 会向每个分支发指令。全部成功就通知各分支删除undo_log中对应的记录这是清理动作。一旦有分支失败就通知所有分支根据undo_log生成反向 SQL。比如扣库存的 UPDATE 前镜像 count 是 1后镜像 count 是 0回滚时 Seata 就会执行UPDATE t_storage SET count 1 WHERE id 1;这个补偿动作在业务代码里完全透明你看到的结果就是库存又回来了。补偿成功的前提是回滚期间这行数据没有被其他事务改写否则会报镜像校验失败需要人工介入。4.3 全局锁防止两个全局事务互相踩踏第一阶段本地事务提交后数据实际上已经可见但 Seata 会在lock_table里记录这行数据被某个全局事务的某个分支锁定。另一个事务要修改同一行必须先等锁。很多时候看到Global lock wait timeout就是两个并发请求同时改同一行库存后到的分支在等待全局锁等太久了就超时。这个报错其实是正常保护机制在工作说明全局锁逻辑已经生效问题出现在业务并发设计上。4.4 数据可见性与隔离级别由于一阶段的本地事务已经提交其他系统直接查库可能读到“全局事务还没最终提交”的中间数据。Seata AT 模式默认全局隔离级别接近读未提交要做到读已提交需要使用SELECT FOR UPDATE或者在业务层加锁来规避。我在实际项目里的处理是对外提供的查询接口尽量走缓存或异步刷新不直接依赖正在变更的库表确有必要读实时数据的就明确告知会有短暂不一致。绝大多数业务场景都能接受这个权衡。5. 可复现示例跨库下单扣库存扣余额的完整验证5.1 服务与库的划分为了验证整套链路我把模拟项目拆成三个服务对应三个独立库。服务数据库核心表order-serviceds_ordert_orderstorage-serviceds_storaget_storageaccount-serviceds_accountt_account建表语句保持精简CREATE TABLE t_order ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) DEFAULT NULL, product_id bigint(20) DEFAULT NULL, amount decimal(10,2) DEFAULT NULL, status tinyint(4) DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8; CREATE TABLE t_storage ( id bigint(20) NOT NULL AUTO_INCREMENT, product_id bigint(20) DEFAULT NULL, count int(11) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8; CREATE TABLE t_account ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) DEFAULT NULL, balance decimal(10,2) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8;每个库各建一张undo_log表初始数据准备一件商品库存 10一个用户余额 100.00。5.2 调用链与注解位置GlobalTransactional必须放在事务发起方的最外层实现方法上我一般放在 order-service 的 ServiceImpl 方法上Override GlobalTransactional(name create-order, timeoutMills 30000) public void createOrder(OrderDTO dto) { orderMapper.insert(dto.toEntity()); storageFeign.deduct(dto.getProductId(), dto.getCount()); accountFeign.debit(dto.getUserId(), dto.getAmount()); }storage-service 和 account-service 的内部方法只加本地事务注解Override Transactional(rollbackFor Exception.class) public void deduct(Long productId, Integer count) { storageMapper.deduct(productId, count); }Feign 调用会自动传递 XID这依赖 Spring Cloud Alibaba 的 Seata 拦截器发送方把 XID 放进请求头接收方从请求头取出来绑定到新线程。如果你不是用 Feign 而是用裸RestTemplate要自己处理这个传递否则第二个分支会找不到全局事务。5.3 正常流程验证调用下单接口后分别到三个库确认数据。正常预期是三张表都发生变化SELECT * FROM ds_order.t_order; SELECT * FROM ds_storage.t_storage WHERE product_id 1; SELECT * FROM ds_account.t_account WHERE user_id 1;能看到订单表多一行、库存从 10 变成 9、余额从 100 变成 80。同时undo_log里出现过记录但提交后会被清理所以最终查出来可能为空。5.4 异常回滚验证在 account-service 的扣款方法里在 UPDATE 执行之后手动抛一个运行时异常Override Transactional(rollbackFor Exception.class) public void debit(Long userId, BigDecimal amount) { accountMapper.debit(userId, amount); throw new RuntimeException(mock balance not enough); }这时候整个链路会全部回滚。调用方捕获到异常后再去查三个库预期结果订单表没有新增记录、库存恢复为 10、余额恢复为 100。这个结果能出来就说明分布式事务真正生效了。5.5 从日志确认链路除了看数据库也要学会看日志。Seata Server 日志里会出现全局事务的提交或回滚记录客户端日志里能看到分支注册和分支事务的提交/回滚。把日志和数据库结果结合起来才能确定是某个分支没执行还是执行了但回滚失败。6. 高频报错与排障思路从日志反推配置问题6.1 No available service default found这个报错几乎每个第一次搭 Seata 的人都见过原因是客户端根据事务分组去 Nacos 找后端服务时找的是default集群但没找到。排查链路我建议固定成一套确认 Nacos 服务列表里存在seata-server没有就回到 Server 的 registry.conf。确认 Server 的 cluster 是default如果改过集群名所有配置要同步改。确认service.vgroupMapping.my_test_tx_groupdefault这条配置在 Nacos 里存在且 group 是客户端 config 指向的SEATA_GROUP。确认客户端registry.conf的 serverAddr 和 namespace 与 Server 一致。这四步按顺序走完基本能解决九成“找不到服务”的问题。6.2 Global lock wait timeout报错长这样Global lock wait timeout, xid..., branchTx...。原因通常是并发更新同一行数据或者上一个大事务迟迟没结束。我先看lock_tableSELECT * FROM seata.lock_table ORDER BY gmt_create DESC;如果锁表里残留大量记录说明有事务一直没结束。再看对应的全局事务是不是已经超时必要时把超时时间调大一些但从根上解决还是要缩短全局事务的执行时间把耗时的外部调用移出去。6.3 undo_log 相关 SQL 异常报错可能有两种Table undo_log doesnt exist和Batch update returned unexpected row count。前者简单就是某个业务库漏建了undo_log表。后者严重一些说明回滚时 Seata 根据前镜像去恢复数据发现当前数据已经变了。最常见原因是回滚前有其他事务改了同一行数据。遇到这种情况要人工核对数据并把账补平不能假装重放一次就能解决。6.4 nacos-client 版本冲突Seata 1.2 传递依赖的 Nacos 客户端版本通常不是 1.2.1而 Spring Cloud Alibaba 2.2.0 希望用 1.2.1。两个版本同时存在时可能抛出NacosException: endpoint is blank或者ClassNotFoundException: com.alibaba.nacos.api.config.ConfigService。处理方式是在 Maven 依赖管理里统一 Nacos 客户端版本dependency groupIdcom.alibaba.nacos/groupId artifactIdnacos-client/artifactId version1.2.1/version /dependency统一之后重新打包确认依赖树里只剩一个 nacos-client。6.5 事务完全没生效时的排查顺序最气人的场景是代码都写对了GlobalTransactional也加了但异常发生后数据没回滚。我现在遇到这种问题会按固定顺序排查看请求日志里有没有 XID 出现并且是否从 order-service 传到了 storage-service、account-service。看业务库undo_log最近有没有写入记录。如果完全没写说明分支事务根本没进入 Seata问题大概率出在数据源没有经过DataSourceProxy或者客户端没连上 Server。看 Seata Server 日志里有没有全局事务记录。没有记录说明全局事务根本没创建。最后回头看注解位置GlobalTransactional是否加在了 Feign 接口上而没加在实现类上。我现在的习惯是先在每个参与库的undo_log查最近 10 分钟记录没记录就回头查代理和注册有记录再看 Server 日志。按这个顺序做绝大多数“事务不生效”的问题都能在十分钟内定位到根因而不是被一堆配置项绕晕。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑