资讯动态

Seata+Nacos分布式事务搭建全复盘:从原理到踩坑

发布时间:2026/10/14 9:10:11 来源:尧图企业网站定制
做微服务项目的人迟早会在某个夜深人静的晚上被分布式事务咬一口。单体时代一个事务就能搞定的事拆成订单、库存、账户三个服务之后光是“下单成功但扣库存失败”这一个问题就够让人翻来覆去睡不着。Spring Cloud Alibaba生态里Seata加Nacos是我用得最多、也最顺手的一套解法Seata专门负责把散落在多个服务里的本地事务编排成一个全局事务Nacos负责服务发现和配置管理。这篇文章就是一次完整搭建过程的复盘从架构原理、版本选型到Seata Server部署、业务服务接入再到我实际踩过的坑一路写下来。适合正在做Spring Cloud项目、准备接入分布式事务的开发者尤其是第一次接触Seata的人。我不打算只列命令会把搭建过程中那些容易懵的“为什么”也讲清楚。比如为什么要改registry.conf、为什么要建undo_log表、为什么vgroupMapping配错就连不上TC理解了这些底层逻辑就算版本升级、环境变化你也能自己排查。1. 先想清楚为什么是Seata Nacos这套组合1.1 分布式事务的本质从“一个库搞定”到“多个服务一起搞定”单体应用里下单、扣库存、减余额都在同一个数据库连接里BEGIN TRANSACTION到COMMIT要么全成功要么全失败数据库自己就能保证。拆成微服务之后数据库连接分属不同进程甚至不同机房本地事务根本管不到别人家的连接。这时候为了保证最终一致只能引入一个独立的协调者来统一决策这就是分布式事务框架存在的意义。生活化一点讲本地事务像一个人在自己家里记账账对不上随时能改分布式事务像是几个分店之间对账总得有个人负责协调“哪个单子算成功、哪个单子要作废”。没有这个协调者每个分店都觉得自己那本账没错合在一起就是一团乱麻。1.2 Seata做的三件事TM、RM、TCSeata的核心角色有三个记不住这三个角色就理解不了后面的配置角色全称干的事类比TCTransaction Coordinator事务协调器独立部署的Seata Server维护全局事务和分支事务的状态中心裁判TMTransaction Manager事务管理器嵌入业务代码里负责开启、提交、回滚全局事务发起人RMResource Manager资源管理器管理分支事务负责执行分支SQL并上报状态各分店负责人第一次接触会觉得这些名词绕但真正用起来你只需要在业务方法上打一个GlobalTransactional注解Seata会自动帮你完成后面所有事情。AT模式的精髓在于对业务代码无侵入你照常写SQL、照常提交本地事务Seata通过数据源代理拦截SQL、记录undo_log二阶段如果发现要回滚就根据undo_log里保存的before image反向恢复数据。这才是它跟XA、TCC最大的区别。1.3 为什么不选XA、TCC、MQ事务消息我在选型时把主流方案都过了一遍这里直接说结论。方案核心思路优点典型问题XA 2PC数据库层面对分布式事务做两阶段提交强一致数据库原生支持资源锁定时间长并发低接入重很多中间件支持不完整TCC业务上手动实现Try、Confirm、Cancel三个操作适合高性能场景隔离好每个业务都要手写大量补偿逻辑开发成本翻倍本地消息表把事务操作和消息写入同一个本地事务再异步通知下游简单直观对业务表侵入强消息表要自己管理MQ事务消息发送半消息事务提交后确认消息异步最终一致削峰填谷只能保证最终一致不适合强一致场景Seata AT数据源代理undo_log自动生成回滚SQL业务无侵入只需加注解要求SQL可解析、表必须有主键全局锁对热点行有竞争综合来看中小团队、业务代码友好度、与Spring Cloud Alibaba生态的契合度Seata AT模式加上Nacos是性价比最高的选择。但我也要泼一盆冷水AT模式不是万能的SQL必须能被Seata解析成可回滚的形式不能用DDL、表必须有主键、不能是无条件大范围UPDATE对数据库隔离级别和全局锁竞争也有要求。知道边界在哪比知道怎么搭更重要。2. 动手前定调版本选型与整体架构2.1 版本兼容性第一步先踩坑做这套搭建最大的坑不是配置难写而是版本之间互相不兼容。Spring Boot、Spring Cloud、Spring Cloud Alibaba、Seata、Nacos这几个东西是绑定的闭着眼睛各装最新版大概率启动就翻车。我实测比较稳的一套组合组件推荐版本说明Spring Boot2.6.x别急着上3.xSpring Cloud Alibaba对Spring Boot 3的支持路线不同Spring Cloud2021.0.x与Spring Boot 2.6对应Spring Cloud Alibaba2021.0.4.0或2021.0.5.0内置了Seata相关整合Seata1.4.21.4.x配置体系最典型资料最多Nacos2.2.32.x相比1.x多了gRPC端口注意放行MySQL5.7或8.x需要InnoDB引擎需要特别提醒的是Spring Cloud Alibaba的starter内部会带一个Seata版本但实际用的时候我建议你显式声明io.seata:seata-spring-boot-starter的版本用mvn dependency:tree核对一下最终生效的版本。如果不核对你项目里跑的可能是starter传递进来的旧版本客户端和服务端版本不一致会出现连上但事务不工作的诡异状况。1.4.x客户端连1.4.x服务端没问题连2.x服务端就会协议不兼容。2.2 TC/TM/RM三种角色怎么协作知道了版本再看运行链路。一次完整的Seata全局事务按照这样的顺序走TM向TC发起Begin Global TransactionTC生成一个全局事务IDXID返回给客户端。XID通过调用链传递到各个服务。Feign调用会自动携带你不需要额外处理。服务执行本地SQL时RM通过数据源代理拦截SQL生成before image和after image写入undo_log然后提交本地事务。本地事务提交后RM向TC注册分支事务。TM在所有业务执行完成后向TC发起全局提交或回滚请求。全部成功TC让各RM异步清理undo_log任一失败TC让各RM根据undo_log反向补偿把数据恢复到before image的状态。这个流程最关键的一点是一阶段本地事务是真实提交的不是像XA那样一直锁住资源。正因如此Seata的并发性能比XA好但也因为有undo_log兜底回滚时才能把“已经提交的本地事务”拉回来。理解这个逻辑你就知道为什么undo_log表绝对不能丢为什么参与事务的表必须有主键。2.3 配置体系registry.conf、config.txt到底谁管谁Seata的配置体系是新手最容易混乱的地方。我用一句话说明白registry.conf管的是“Seata Server要去哪里注册自己、去哪里读配置”config.txt里才是真正的“事务参数”。Seata Server启动时先读registry.conf发现registry.typenacos就把自己注册到Nacos同时发现config.typenacos就从Nacos拉取事务配置。而业务服务端的配置则在application.yml里通过seata.registry和seata.config两段告诉Seata客户端“你去找哪个Nacos”。两边的namespace、group、dataId必须对上对不上就会出现服务注册成功但事务起不来的古怪现象。3. Seata Server从零部署Nacos与数据库准备3.1 启动Nacos并初始化Seata存储库第一步是先把Nacos跑起来。我用的是2.2.3版本单机模式启动sh startup.sh -m standalone启动之后确认控制台能访问默认地址http://127.0.0.1:8848/nacos账号密码都是nacos。Nacos 2.x有个容易忽略的细节客户端连接除了8848端口还需要9848这个gRPC端口防火墙或云安全组只放行8848的话服务能注册但配置会间歇性读取失败。接下来要在MySQL里建一个Seata自己的库。这个库不是业务库是用来存全局事务会话的名字一般就叫seataCREATE DATABASE IF NOT EXISTS seata DEFAULT CHARACTER SET utf8mb4; USE seata; 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(128), transaction_id BIGINT, branch_id BIGINT NOT NULL, 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;这四张表的作用要心里有数global_table存全局事务状态branch_table存分支事务状态lock_table存全局锁undo_log表在业务库里。如果用的是Seata 1.5以上版本官方脚本还会多一张distributed_lock表建表时以你下载版本自带的脚本为准。3.2 修改registry.conf把Seata Server挂到NacosSeata Server下载解压之后找到conf/registry.conf。默认是file模式我们要改成Nacosregistry { type nacos nacos { application seata-server serverAddr 127.0.0.1:8848 group SEATA_GROUP namespace cluster default } } config { type nacos nacos { serverAddr 127.0.0.1:8848 group SEATA_GROUP namespace dataId seataServer.properties } }这里有几个关键点先记住namespace是Nacos的命名空间ID不是名称。默认public命名空间就留空字符串。group默认是SEATA_GROUP业务服务端配置group时必须一致否则配置拉不到。cluster是集群名默认default。业务端的vgroupMapping映射值就指向这个cluster。如果你生产环境有多个环境dev/test/prod建议每个环境用独立的namespace和group隔离别把所有环境混在一个namespace里否则配置容易串。3.3 把Seata配置灌进Nacosconfig.txt与脚本执行Seata Server从Nacos拉取事务配置但这些配置不是手敲到Nacos控制台的而是通过下载包里自带的config.txt和nacos-config.sh脚本上传。先看conf/config.txt里几个必须改的项store.modedb store.db.datasourcedruid store.db.db-typemysql store.db.driver-class-namecom.mysql.cj.jdbc.Driver store.db.urljdbc:mysql://127.0.0.1:3306/seata?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai store.db.userroot store.db.password你的数据库密码 service.vgroupMapping.my_test_tx_groupdefaultstore.modedb意思是让TC把全局事务会话持久化到数据库而不是默认的file文件。生产环境必须用db模式原因很简单file模式单点存储Seata Server重启后事务会话可能丢失db模式多节点共享一份存储才支持高可用。service.vgroupMapping.my_test_tx_groupdefault这一行定义了事务分组到集群的映射my_test_tx_group是事务分组名default对应registry.conf里的cluster名。这个映射关系业务端配置时也要用同一个事务分组名。改完之后执行上传脚本cd script/config-center/nacos sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -t 你的namespaceID脚本会把config.txt的内容合并成seataServer.properties这个dataId上传到Nacos。上传完成后记得去Nacos控制台看一眼配置列表确认配置真的进去了这一步经常因为namespace ID填错或者脚本权限问题静默失败。3.4 启动Seata Server并验证准备工作做完启动Seata Serversh seata-server.sh -p 8091 -h 127.0.0.1 -m db -n 1参数含义-p 8091指定TC端口-h配置注册到Nacos的地址多网卡机器一定要显式指定否则可能注册成内网地址或错误地址-m db指定存储模式-n 1是节点编号多节点部署时每个节点用不同编号区分。启动后观察日志确认没报错再去Nacos控制台的“服务列表”里看能看到一个名为seata-server的服务实例说明Server已经成功注册到Nacos。这里还有一个验证技巧故意在业务服务还没配好的情况下只启动Seata Server日志里如果出现连接Nacos异常、读取配置超时先回头查registry.conf和config.txt别急着写业务代码。4. 业务服务接入两个服务的下单案例4.1 案例结构order-service与inventory-service现在进入业务侧。我用一个典型的交易链路做例子order-service订单服务端口8081和inventory-service库存服务端口8082。用户下单时先创建订单再通过Feign调用库存服务扣减库存。如果扣库存失败订单记录要一并回滚。两个服务都有各自的数据库物理上无法用本地事务解决就得靠Seata。order-service ----- inventory-service | | 订单表 tbl_order 库存表 tbl_stock 状态表 tbl_order_status4.2 依赖与版本锁定两个服务的pom.xml里先通过Spring Cloud Alibaba的BOM统一版本dependencyManagement dependencies dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version2021.0.4.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后单独引入Seata依赖dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-seata/artifactId /dependency dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.4.2/version /dependency第二个依赖是显式锁版本的关键。如果只用starter它内部带的Seata版本可能不是你预期的那一个服务端和客户端版本一旦错位会出现“连接正常但全局事务不生效”的隐蔽问题。引入后执行一遍mvn dependency:tree | grep seata确认最终版本。4.3 配置application.ymlvgroupMapping是命门每个业务服务的application.yml里除了Nacos注册中心的常规配置还要加一段Seata配置spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 seata: enabled: true application-id: ${spring.application.name} tx-service-group: my_test_tx_group registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP cluster: default config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP >CREATE TABLE IF NOT EXISTS 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) ) ENGINE InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET utf8mb4;这张表是AT模式回滚的依据。一阶段本地事务提交前Seata会把before image、after image、XID等信息写进undo_log二阶段回滚时根据undo_log里的内容生成反向SQL。表名、字段都不能改业务库是哪个就建到哪个库里。我见过有人只在主库建、在从库漏建的结果读写分离环境下回滚时报找不到表排查半天。数据源代理这块Seata 1.4.2在>Configuration public class DataSourceProxyConfig { Primary Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } }这个Bean加了Primary让Seata代理数据源优先被注入。多数据源场景下手动代理能避免“某个数据源没被代理、SQL没进undo_log”的隐患。4.5 核心链路加注解、走Feign、观察回滚先看接口定义。库存服务提供扣减接口RestController RequestMapping(/inventory) public class InventoryController { Resource private StockService stockService; PostMapping(/deduct) public void deduct(RequestBody DeductRequest request) { stockService.deduct(request.getSkuId(), request.getCount()); } }订单服务通过Feign调用FeignClient(name inventory-service) public interface InventoryClient { PostMapping(/inventory/deduct) void deduct(RequestBody DeductRequest request); }关键的全局事务入口在订单服务的实现方法上Service public class OrderServiceImpl implements OrderService { Resource private OrderMapper orderMapper; Resource private InventoryClient inventoryClient; Override GlobalTransactional(rollbackFor Exception.class) public void createOrder(OrderDTO dto) { orderMapper.insert(buildOrder(dto)); inventoryClient.deduct(new DeductRequest(dto.getSkuId(), dto.getCount())); } }执行过程是这样的createOrder被GlobalTransactional标记Seata的TM向TC注册全局事务并拿到XIDFeign调用时Seata的拦截器自动把XID放到请求头里传给inventory-service两个服务各自执行本地SQL各自生成undo_log并提交本地事务最后TM根据方法执行结果决定全局提交或回滚。想验证回滚最简单的办法是在扣库存方法里人为抛一个异常public void deduct(Long skuId, Integer count) { stockMapper.deduct(skuId, count); throw new RuntimeException(故意扣库存失败); }这时再看数据库库存表的扣减数据已经被Seata根据undo_log恢复原状订单表新插入的订单也会被删除。如果订单还留在表里说明XID没传过去或者某个服务的数据源没被代理按这个方向去查。5. 实战高频踩坑与排查实录5.1 问题速查表从报错到根因搭建过程中遇到的大部分问题都集中在几个固定位置。我整理了一个速查表基本覆盖了90%的报错场景现象/报错根因解决方案调用全局事务方法时报can not connect to services-servervgroupMapping映射值、Nacos的namespace、group、cluster不一致检查seata.service.vgroup-mapping的value与Seata Server的cluster是否都为default检查namespace和group业务库报table undo_log doesnt exist参与事务的业务库没建undo_log表每个业务库都要建字段不能改报Global lock wait timeout多个全局事务并发更新同一行记录全局锁竞争调大client.rm.lock.retry-times和retry-interval或从业务上降低热点行冲突报need primary key参与事务的表没有主键Seata无法定位行给表加主键AT模式硬性要求客户端连不上Nacos或配置间歇性读取失败Nacos 2.x的gRPC端口9848未放行防火墙、安全组放开9848端口Feign调用时XID没有传递自定义了Feign拦截器覆盖了Seata默认拦截器或用了RestTemplate保留Seata的Feign拦截器RestTemplate手动加SeataRestTemplateInterceptorSeata Server启动成功但Nacos服务列表看不到registry.conf中namespace填了名称而非ID或serverAddr写错核对namespace为ID确认serverAddr可访问回滚不生效甚至回滚时报SQL错误参与事务的表结构被ALTER过before image和after image对不上全局事务期间不要做DDL回滚失败后人工核对数据全局事务不结束global_table记录不清理TM超时配置过长或业务线程阻塞事务长时间不提交检查client.tm.timeout排查业务方法是否卡在远程调用上5.2 定位问题优先看这三个地方遇到分布式事务问题我先看日志、再看注册表、最后看数据库会话这个顺序能省大量时间。第一是客户端日志。在application.yml里把Seata日志级别调成debuglogging: level: io.seata: debug然后搜xid关键字能看到全局事务从哪里开始、分支事务在哪里注册、最终是commit还是rollback。这一步基本能判断问题出在TM、RM还是TC。第二是Nacos控制台的服务列表。确认Seata Server在线确认业务服务也注册了。如果业务服务在但要通过Seata跨服务调用时找不到TC问题就在vgroupMapping或cluster配置。第三是数据库。看Seata库的global_table、branch_table、lock_table能知道事务卡在哪个阶段看业务库的undo_log表能知道哪个分支生成了回滚日志、是否被清理。我遇到过一种很难查的情况全局事务一直没结束表面看业务正常一查global_table发现很多记录停留在初始状态最后定位到是TM超时设置太长加上Feign调用超时事务挂着不释放。5.3 上线前要把这几个参数调明白链路通了之后别急着上生产先把几个关键参数调明白。seata: client: rm: lock: retry-times: 60 retry-interval: 10ms tm: timeout: 60000client.rm.lock.retry-times和retry-interval控制全局锁等待重试。默认值较小热点行冲突时容易报锁等待超时。client.tm.timeout是全局事务超时时间单位毫秒默认60000。这个值要根据业务实际耗时来定设置太短正常的慢接口会被误判超时设置太长有问题的链路会长时间占着TC资源。我的习惯是先按业务最慢峰值加50%余量线上跑一段时间再收紧。还有一个容易被忽略的点Seata Server端用的MySQL连接池大小。TC的并发事务处理能力直接受store.db.max-conn影响部署机并发高的话适当调大观察TC线程和数据库连接数再做微调。写在最后的实操体会最后说点我自己的感受。第一次搭这套组合的时候我建议你把Seata Server和两个业务服务跑起来先用一个最简单的下单接口把全局事务链路走通再往上加业务。链路没通之前不要急着调参数调了也看不出来效果。踩过几次坑之后我最大的体会是Seata AT模式解决的是“普通业务代码里的分布式事务”这是绝大多数微服务项目最需要的但凡涉及大批量数据修正、复杂存储过程、跨库查询的服务都要重新评估是否适合用AT模式。它不需要你重写业务但需要你理解它的边界。希望这份复盘能帮正在折腾Spring Cloud Alibaba、Seata、Nacos这套组合的人少绕几个弯。

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

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

免费获取报价 →
↑