项目标题虽然看起来只是一个版本号的堆砌但背后其实是微服务架构里最绕不开的硬骨头——分布式事务。如果你手上正捏着 Spring Cloud Alibaba 2.2.0 Nacos 1.2.1 这套组合还在为 Seata 1.2 怎么接进来、怎么让跨库跨服务的数据保持一致而头疼那这篇文章就是冲着你来的。我用一个订单创建的完整场景订单服务 库存服务 账户服务把整个链路串起来从 Seata Server 部署、Nacos 配置导入、数据库 undo_log 表准备到业务代码里加GlobalTransactional注解再到 AT 模式底层原理和实操中一定会踩的坑一次性讲透。这套组合虽然版本不算新但胜在稳定、资料多、网上案例丰富至今仍是很多中小企业微服务改造的起步标配。如果你正在做技术选型或者在已有项目里接 Seata这篇文章可以直接拿来当落地手册用。1. 整体设计与方案拆解1.1 为什么是这套版本组合先说结论Spring Cloud Alibaba 2.2.0 对应的 Spring Boot 版本是 2.2.xSpring Cloud 版本是 Hoxton这个时期刚好是 Alibaba 组件生态在国内大规模普及的阶段Nacos 1.2.1 和 Seata 1.2 都处在一个功能稳定、社区讨论度最高的状态。很多人在搭建微服务时都会问为什么不用最新版我的观点很直接——分布式事务这种核心链路稳比新重要。Seata 从 1.0 到 1.2 完成了 AT 模式的成熟沉淀而 1.2 的配置方式和 Nacos 的集成体验刚好达到一个平衡点。再往后的版本虽然引入更多特性但配置项、依赖坐标都发生过调整网上能搜到的解决方案反而不如 1.2 丰富。从实际架构来说这套组合的价值在于Nacos 同时承担注册中心和配置中心两个角色服务发现与配置管理一体化Seata Server 自身注册到 Nacos业务服务通过 Nacos 找到 Seata Server 地址Spring Cloud Alibaba 提供了与 Nacos、Seata 无缝衔接的 starter不需要自己造轮子。换句话说这套组合解决的问题很直接在有多个数据库、多个服务的情况下保证一次业务操作要么全部成功、要么全部回滚不出现“订单建了但库存扣了钱没扣”这类数据不一致问题。1.2 数据一致性问题的本质在单体应用时代一个订单方法里可以同时操作订单表、库存表、账户表一个数据库事务就能保证原子性。微服务拆开后订单、库存、账户各归各的库本地事务已经无能为力这个时候就必须引入分布式事务方案。分布式事务的核心矛盾在于多个独立的资源管理器数据库之间如何协调提交或回滚。常见方案有 XA 两阶段提交、TCC 补偿、消息最终一致性而 Seata 的 AT 模式走的是“两阶段提交的改良版”——第一阶段直接提交本地事务并记录回滚日志第二阶段异步协调全局提交或回滚。AT 模式最吸引人的地方在于对业务代码侵入极小。你不需要像 TCC 那样写 confirm 和 cancel 方法只需要在发起全局事务的方法上加一个注解Seata 会自动拦截 SQL、生成 undo_log、协调分支事务。从开发效率角度来说这是目前性价比最高的一种分布式事务落地方式。2. Seata Server 搭建与 Nacos 集成2.1 环境准备与版本对应关系为了避免后面出现各种莫名其妙的兼容性问题我先把这套环境的版本对应关系列出来照着配就能少踩一半的坑。组件版本说明Spring Boot2.2.x2.2.5 实测比较稳Spring CloudHoxton.SR9与 Spring Boot 2.2.x 配套Spring Cloud Alibaba2.2.0.RELEASE注意这个版本号很容易和别的东西混淆Nacos Server1.2.1同时用作注册中心和配置中心Seata Server1.2.0内置在 seata-server 发布包中数据库MySQL 5.7需要 InnoDB 引擎AT 模式依赖行锁与 undo_logJDK1.8这套组件对 JDK 11 以上的支持当时还不完善老老实实用 8有一个特别容易忽略的点Spring Cloud Alibaba 2.2.0 里有一部分组件还是孵化期状态别在 pom 里把整个依赖树不加筛选地引入用哪个引哪个否则模块之间版本冲突会让你怀疑人生。2.2 Seata Server 部署与 Nacos 配置导入Seata Server 部署本身不复杂下载官方发布包解压后重点要改两处conf/registry.conf和conf/file.conf。1.2 版本默认支持从文件读配置但我们既然有 Nacos就直接把配置中心切到 Nacos统一管理。registry.conf的核心配置如下registry { 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 namespace group SEATA_GROUP } }这两个配置块一个管注册Server 启动后把自己注册到 Nacos一个管配置Server 自身的事务规则从 Nacos 拉取。如果 Nacos 设了命名空间这里要保持一致最好在 Nacos 里单独建一个SEATA_GROUP分组避免和业务配置混在一起。file.conf是事务存储配置重点修改 store 部分。开发环境可以直接用 file 模式但生产环境更推荐 db 模式让事务信息持久化到数据库里避免 Seata Server 重启后全局事务状态丢失。db 模式需要先建一张global_table、branch_table和lock_table官方 SQL 脚本在seata-server-1.2.0/conf/db_store.sql里可以直接找到。改完这两个文件还需要把 Seata 的配置项推送到 Nacos。Seata 1.2 提供了一个config.txt模板里面定义了事务超时时间、提交重试次数、回滚重试次数等参数。手动导入太麻烦可以直接用官方提供的脚本nacos-config.sh批量导入sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP导入完成后启动 Seata Server观察启动日志确认注册成功The server[seata-server] connected to registry successfully这个日志说明 Seata Server 已经注册到 Nacos后面业务服务才能通过注册中心找到它。2.3 数据库初始化的准备工作Seata AT 模式要求每个参与分布式事务的数据库都必须有一张undo_log表这张表是回滚的数据基础。表结构官方已经给了直接复制执行即可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, ext VARCHAR(100) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid, branch_id) ) ENGINE InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET utf8;记住是每个业务数据库都要建不是只建在主库。订单库、库存库、账户库三张库各来一份。漏掉任何一个执行到那个服务时就会报Table undo_log doesnt exist全局事务直接失败。3. 业务系统改造与核心代码实现3.1 订单场景的需求定义为了把整个流程讲清楚我设计了这样一个模拟场景用户下订单总共需要完成三件事——在订单库插入一条订单记录、在库存库扣减商品库存、在账户库扣减用户余额。这三个操作分布在三个服务里order-service端口 8081负责创建订单inventory-service端口 8082负责扣库存account-service端口 8083负责扣余额。业务流程上订单服务是入口它先创建订单然后通过 Feign 调用库存服务扣库存再调用账户服务扣余额。任何一个环节抛异常已经执行的本地事务都要回滚。3.2 服务端 Maven 依赖与配置每个服务需要引入的公共依赖大致如下dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-seata/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency这里有个版本埋坑点spring-cloud-starter-alibaba-seata这个坐标里默认引入的 seata 版本可能不是你想要的 1.2最好显式指定 seata 版本。dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.2.0/version /dependency另外1.2 版本要求每个业务服务里都有一个file.conf和registry.conf用来指定 Seata Server 的注册地址。你可以从 Seata Server 的 conf 目录直接拷贝一份放到服务端resources下但注册方式要改成 nacosregistry { type nacos nacos { serverAddr 127.0.0.1:8848 } }如果你的所有服务都指向同一个 Seata Server这个文件每个服务都要放内容一样也行但注意不要用相同的application否则在 Seata Server 端会出现多个同名客户端的情况。业务应用的application.yml配置如下spring: datasource: url: jdbc:mysql://127.0.0.1:3306/order_db?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver cloud: nacos: discovery: server-addr: 127.0.0.1:8848 seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group enable-auto-data-source-proxy: true service: vgroup-mapping: my_test_tx_group: default registry: type: nacos nacos: server-addr: 127.0.0.1:8848这里的tx-service-group是全局事务分组名可以自定义但必须和 Seata Server 内配置的service.vgroupMapping一致。通俗理解就是业务服务说“我属于 my_test_tx_group 这个事务组”Seata Server 根据映射关系找到对应的 Seata Server 集群来处理事务。3.3 核心业务代码实现订单服务的实现代码是整套流程的中枢。在入口方法上加上GlobalTransactional注解Seata 就会把这个方法纳入全局事务管理。Service public class OrderServiceImpl implements OrderService { Autowired private OrderDao orderDao; Autowired private InventoryFeignClient inventoryClient; Autowired private AccountFeignClient accountClient; Override GlobalTransactional(name create-order, rollbackFor Exception.class) public void createOrder(OrderDTO order) { // 1. 本地插入订单数据 orderDao.insert(order); // 2. 远程调用库存服务扣减库存 inventoryClient.decreaseStock(order.getProductId(), order.getCount()); // 3. 远程调用账户服务扣减余额 accountClient.decreaseBalance(order.getUserId(), order.getAmount()); } }注意rollbackFor Exception.class这个属性。GlobalTransactional默认只在遇到RuntimeException时才回滚如果你业务里抛的是自定义受检异常不加这个配置就永远不会触发回滚这是最容易踩的坑之一。库存服务核心逻辑Service public class InventoryServiceImpl implements InventoryService { Autowired private InventoryDao inventoryDao; Override Transactional(rollbackFor Exception.class) public void decreaseStock(Long productId, Integer count) { // 扣减库存模拟超卖场景可以用乐观锁 int result inventoryDao.decreaseStock(productId, count); if (result 0) { throw new RuntimeException(库存不足); } } }账户服务核心逻辑类似扣余额时如果余额不足直接抛异常Override Transactional(rollbackFor Exception.class) public void decreaseBalance(Long userId, BigDecimal amount) { int result accountDao.decreaseBalance(userId, amount); if (result 0) { throw new RuntimeException(余额不足); } }需要说明一点被远程调用的服务不加GlobalTransactional它们只需要本地事务配合即可。全局事务的角色由发起方扮演这一点很多初学者会搞混。Feign 接口这里不再赘述和普通的微服务调用写法完全一致关键在于链路中的每个服务都要被 Nacos 正常注册发现。3.4 验证链路是否打通启动三个服务后先确认 Nacos 控制台的服务列表中能看到order-service、inventory-service、account-service和seata-server四个服务。然后调用订单接口比如curl -X POST http://localhost:8081/order/create -H Content-Type: application/json -d {userId: 1, productId: 1, count: 2, amount: 200}正常情况下三张表都会有对应的数据。打开 Seata Server 的日志能看到类似下面的输出io.seata.core.protocol.transaction.GlobalBeginResponse - xid 192.168.1.100:8091:123456789这个全局事务编号会贯穿整个调用链路下游服务的日志里也能搜到同一个 xid。如果想把链路看得更清楚在三个服务的日志中搜索xid关键字能看到全局事务注册分支事务、提交分支事务的完整过程。4. AT 模式执行链路与原理解析4.1 一阶段提交本地事务先行很多第一次接触 Seata 的人会困惑既然叫分布式事务为什么每个服务还要加本地事务Transactional这不是重复吗其实这正是 AT 模式的设计精妙之处。拿上面订单创建的案例来说当OrderService.createOrder被GlobalTransactional拦截后Seata 会向 Server 注册全局事务并生成 xid。之后每个参与的服务执行本地 SQL 时Seata 的数据源代理会对 SQL 做两件事解析 SQL 类型INSERT、UPDATE、DELETE在执行业务 SQL 前先把涉及行的原始数据before image和预计更新后的数据after image写入undo_log表。然后业务 SQL 和写入 undo_log 这两个操作作为同一个本地事务一起提交。也就是说一阶段结束后数据其实已经真实生效了这在性能上远优于 XA 模式的“只准备不提交”。4.2 二阶段提交或回滚悄悄擦掉痕迹如果所有分支事务都成功Seata Server 通知各分支提交。此时不需要做任何数据操作只是把对应的undo_log记录删掉因为数据已经是最新的了。如果有任何一个分支事务失败Seata Server 会通知所有已经成功的分支执行回滚。回滚的原理是读取undo_log中的 before image对比当前数据与 after image 是否一致一致说明期间没有被其他事务修改直接按 before image 恢复数据然后删除 undo_log不一致则说明数据被其他事务动过无法自动回滚需要人工介入。这个机制用生活化的比喻来说你把一个文件保存成了 V2 版本Seata 一直都在旁边留着一份 V1 版本的手抄本如果整套动作中途出了问题它就把 V2 删掉把手抄本重新誊写回去外部看起来就像什么都没发生过。4.3 全局锁与隔离级别的取舍AT 模式为了保证回滚时不出现数据错乱引入了一个全局锁的概念。在一阶段执行时Seata 会为涉及的行记录申请全局锁锁的信息存在lock_table表里。其他事务如果也想操作同一行需要先检测全局锁是否被占用。这里的隔离级别默认是读未提交级别因为一阶段数据已经真实修改了其他事务可能会读到中间态数据。如果你对数据一致性要求极高需要通过select ... for update或业务层面的乐观锁来保证。大多数订单场景其实不要求在事务过程中不让别人读所以这个取舍通常可以接受。但全局锁也确实会带来一个隐患如果某个分支事务执行时间过长全局锁一直不释放下游请求就会阻塞最终出现超时。这也是为什么 Seata 的事务超时时间不要设置得太大一般 60 秒内要完成整套动作否则要么调大超时要么优化业务 SQL 的耗时。5. 常见问题与排查技巧实录5.1 问题速查表下面这些是我在搭建和调试过程中真实遇到、也看到社区里反复出现的高频问题整理成了速查表现象可能原因排查思路Seata Server 启动失败registry.conf 里 Nacos 地址填错检查 8848 端口连通性直接浏览器访问 Nacos 控制台确认服务正常服务启动后报can not connect to seata serverfile.conf 的 registry 配置与服务端不一致检查业务服务的 registry.conf 和 Seata Server 是否指向同一个 Nacos事务调用后没有回滚发起方没加GlobalTransactional或rollbackFor没配置检查注解位置确认是方法级别而不是类级别注意受检异常必须显式声明Table undo_log doesnt exist某个服务的数据库没建 undo_log 表逐个库核对三个库都要有AT 模式回滚时报脏数据错误数据在事务期间被其他线程修改先查 undo_log 的 before image 与当前数据是否一致一致则尝试手动修复分支事务注册失败Seata Server 的vgroupMapping没配对核对服务端和客户端的 tx-service-group 名称5.2 排查回滚是否真正触发的技巧一个很隐蔽的情况是代码里看似加了GlobalTransactional调用也成功了但异常发生后数据没有回滚你却不知道问题出在哪。我的排查习惯是分三步走看发起方日志有没有Begin new global transaction和Global rollback关键字看 Seata Server 日志有没有出现branch rollback记录以及 rollback 结果是成功还是失败查看被调用服务的undo_log表如果回滚正常发生这张表会有 insert 和 delete 的痕迹不会残留数据。如果服务方日志里一直没有 xid 相关输出大概率是 Seata 数据源代理没有生效。检查下seata.enable-auto-data-source-proxy是不是被误设为false或者你的数据源是不是自定义创建的绕过了代理。5.3 性能优化的经验之谈Seata AT 模式最大的性能开销来自镜像数据和全局锁竞争高并发场景下下单接口如果单笔事务超过 100ms就要警惕 Seata Server 成为瓶颈。我个人的优化策略有两条 一是把 Seata 的存储模式切换成 db让全局事务表落在数据库而不是内存这样 Seata Server 可以水平扩展分散压力 二是缩短业务方法的执行时间Feign 调用能并行就并行不要在事务内做耗时的外部 IO 操作。一个全局事务的时间就是你最大容忍的数据不一致窗口越短越好。再说一句很多文章不会提的Seata AT 模式回滚不适用于所有 SQL比如UPDATE ... LIMIT 1这种带 limit 的更新回滚时可能无法精确定位到被改的那一行DDL 语句更不可能被回滚。你在设计业务 SQL 时就要避开这些场景项目上线前用完整链路压一遍回滚逻辑比出了问题再去补救靠谱得多。6. 从搭好到用好我的一点收尾建议这套 Spring Cloud Alibaba 2.2.0 Seata 1.2 Nacos 1.2.1 的组合如果你只是照着教程把服务跑起来大概半天就能完成。但真正做到生产可用我建议你再花点时间做三件事一是用压测模拟高并发场景下的超时回滚确认全局锁竞争不会拖垮你的数据库二是把 Seata 的监控打开它在 1.2 版本里已经有基础的 metrics 能力配合日志能让你对每一条全局事务的执行链路一目了然三是给参与事务的服务补上降级方案分布式事务能解决数据一致性问题但解决不了服务可用性问题分布式系统里永远要假定下游不可用。从踩过无数次坑的经验来说这套方案的落地难度不在技术本身而在你是否真正理解了全局事务的生命周期和数据补偿的边界。理解透了后面无论是升级 Seata 新版本还是迁移到别的分布式事务方案都能很快上手。如果你照着这篇文章搭了一遍建议你用压测工具模拟一次库存不足的极端场景亲眼看一次全链路回滚的过程那个瞬间的理解比看十篇原理分析都管用。
阅读完成 · 觉得有帮助?