Dubbo集成Seata AT模式实战:微服务分布式事务一致性落地

📅 发布时间:2026/10/14 17:32:14
Dubbo集成Seata AT模式实战:微服务分布式事务一致性落地
订单创建这个操作单体时代就是同一个数据库里插订单表、扣库存表、减资金表一个本地事务就完事。微服务拆开之后订单、库存、账户各占一个独立库一次下单要跨三个数据源写数据。前端刚点完支付后端任何一个环节抛异常都会在某个库里留下半截数据最常见的就是订单表有记录但库存没扣或者库存扣了但账户没减。这类账不平的问题就是分布式事务要解决的。Seata的AT模式是这些年大家接受度最高的折中解法配合Dubbo做链路改造时工作量集中在配置和验证上业务代码侵入极小。这篇文章就围绕Dubbo集成Seata AT模式这条主线把版本选型、Server部署、客户端接入、机制原理和踩坑经验都梳理一遍适合已经完成微服务拆分、正在补事务短板的团队参考。1. 先想清楚AT模式到底解决了什么问题1.1 一次扣款事故引出的分布式事务难题刚接触这个问题的同学第一反应往往是数据库不是有事务吗把三个库放同一个事务里不就行了。这在分布式场景下做不到跨库跨连接的Commit是没法保证同时成功的。早期的XA方案可以做到让数据库自己参与两阶段提交代价是事务期间数据库连接被长时间占用性能损耗大而且很多业务库用的中间件和数据库类型还不一样XA的部署成本很高。后来业界普遍转向TCC这类业务侵入式补偿方案让每个参与方自己实现try、confirm、cancel三个方法。TCC确实可行但改造量大得可怕每个业务方法都要额外写两套逻辑普通项目根本背不动这个成本。我见过一个真实案例。某团队做订单系统微服务化代码拆了三周很顺利联调时出了大问题账户服务扣款成功库存服务扣减失败订单表里却已经有了订单。没有分布式事务兜底最终靠着人工跑脚本把库存补回来。那次之后团队才开始认真调研分布式事务方案最后选型就是Seata AT模式。原因很直接AT模式对现有代码的改造量最小不需要像TCC那样重写业务方法也不像XA那样绑死数据库。1.2 AT模式的自动化思路镜像与undo_logAT模式的全称是Automatic Transaction自动事务。它解决“自动”这个问题的核心手段是在一阶段做SQL拦截和镜像记录。举个具体例子你执行一条“update account set balance balance - 100 where id 1”AT模式会先查一次当前数据把balance1000这一行完整记录记为前镜像执行完update后再查一次把balance900记为后镜像两份镜像连同SQL类型一起写入当前业务库的undo_log表最后业务表和undo_log在同一个本地事务里一起提交。这里最关键的一点是业务数据变更和undo_log是同一个本地事务提交的。只要本地事务成功undo_log一定在本地事务失败业务数据也没生效自然不需要回滚。这种“把补偿日志和业务数据放同一个事务”的设计是整个AT模式自动补偿的基石。二阶段如果所有分支都成功就异步清理undo_log只要有一个分支失败就根据undo_log里的前镜像生成反向SQL把数据恢复原样。整个反向补偿过程业务侧完全无感。1.3 用Dubbo做链路时的最大难点事务上下文传递Seata实现全局事务时交易发起方会分配一个全局事务ID也就是XID。后续所有参与分支的数据库操作都要登记到这个XID名下。Dubbo服务之间的远程调用从下游服务再打到下一个服务XID必须跟着请求一路透传。如果哪一段没传过去就会出现全局事务断链分支事务跑到一个孤儿事务里去失败后根本不会被回滚。Dubbo的Filter机制天然适合做这件事。Seata给Dubbo提供了专门的Filter会在发起调用时把XID注入到RPC attachment里消费端取出来放进当前线程上下文。看起来简单真正落地时问题不少。比如Dubbo版本升级后Filter没加载、线程池复用导致上下文串位、异步调用丢上下文等。这些坑我在后面专门展开先说结论集成Seata时客户端一定要引入seata-dubbo这个依赖否则XID透传的Filter根本不会注册到Dubbo容器里业务代码全程不报错但全局事务形同虚设。2. 集成前必须先定下来的几件事2.1 Seata服务端与客户端的版本搭配结论Seata的版本兼容是第一个容易踩雷的地方。Seata Server端和客户端版本必须对齐否则分支事务注册经常报错。我自己踩过Server用1.6.1、客户端用1.5.2结果事务分支一直注册失败日志里各种RPC异常。所以落地的第一准则就是两端用同一个版本号。我测试下来比较稳定的一套搭配是Seata Server 1.8.0 seata-spring-boot-starter 1.8.0 Dubbo 2.7.15 Spring Boot 2.7.x。Dubbo 3.x也能跑但要注意Dubbo Filter的加载方式和attachment的传输逻辑跟2.7有所差异没有特殊要求先用2.7.x最省心。Seata也有2.x版本了但我建议大多数追求稳定的项目先不升大版本1.8.0已经覆盖了AT模式所有常用能力社区资料也最多。Seata客户端依赖有几种组合。Spring Boot项目最常用seata-spring-boot-starter但它只包含核心模块Dubbo集成还需要额外引入seata-dubbo。这个包就是负责加载XID透传Dubbo Filter的。如果不引业务代码全程不会报错但是XID传不过去全局事务形同虚设这是最隐蔽的问题之一。2.2 注册中心、配置中心和事务分组的选择Seata的Server端和Client端都需要注册中心。Server用注册中心对外暴露服务端口让客户端能找到它客户端通过注册中心获得Server列表。最常用的方案是Nacos。如果项目原本没有引入Nacos也可以用file方式把Seata Server地址直接写在客户端配置文件里省去注册中心组件适合小规模单机部署。配置中心负责下发Seata自身的配置项比如事务存储模式、锁重试参数、超时时间。同样可以用Nacos集中管理也可以全部写在本地file.conf里。我的建议是先全用本地file方式把链路跑通再迁移到Nacos减少排查面。把Nacos、Seata、Dubbo、业务库四方同时上马出问题根本分不清是哪一环。事务分组tx-service-group是另一个必须理解的概念。客户端配置的事务分组名比如my_test_tx_group要通过vgroup-mapping映射到Seata Server的集群名比如default。客户端拿着集群名去注册中心找对应的Server。这个映射关系写错最常见的表现就是日志一直打印“no available server to connect”但Nacos里明明有服务。排查时顺着“事务分组 - 集群名 - 注册中心服务名”这条线走就很快。2.3 什么时候不该用AT模式写这个是因为见过太多团队把AT当银弹最后中途放弃。AT模式在两类场景下会很痛苦。第一类是热点资源高频扣减比如秒杀场景下同一个商品库存字段被大量并发更新。AT的全局锁机制会强制同一时刻只有一个全局事务能改这一条记录其他事务阻塞等锁等待时间一长就报“Global lock acquire timeout”业务量一大根本扛不住。第二类是超大表或复杂SQLAT要生成整行数据的前后镜像如果update语句扫了几万行镜像数据量和undo_log写入量会非常夸张性能会被拖垮。如果业务命中这两类场景可以考虑把热点段拆出来单独用TCC或本地事务加MQ最终一致性去处理其余普通链路继续用AT。没有一种分布式事务方案是万能的方案边界比方案本身更值得花时间分析。这也是集成前最该做的事评估业务而不是一上来就搭环境。3. Seata Server部署与客户端接入实操3.1 Seata Server怎么部署起来从Seata官方发布的release包下载seata-server压缩包解压后目录里有两个关键文件registry.conf和file.conf。registry.conf决定注册中心和配置中心类型file.conf是本地配置文件。registry { type nacos nacos { application seata-server serverAddr 127.0.0.1:8848 group SEATA_GROUP namespace cluster default username password } } config { type file file { name file.conf } }如果本地没有Nacos可以把registry.type改成file这样服务端通过file.conf读取所有配置客户端也配同样的file方式。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?useUnicodetruecharacterEncodingutf8 user root password 123456 minConn 5 maxConn 20 } }store.mode有file、db、raft三种。file模式零依赖数据落在一个本地文件里适合测试db模式把全局事务状态写进数据库服务和库一起重启也不会丢生产环境建议用dbraft模式是1.5之后新增的高可用方案至少三节点对运维要求高一些。部署时注意Server端日志默认是info级别排查分支事务注册问题时往往需要临时调成debug可以在seata-server的yml配置里把logback的level改掉。3.2 初始化全局事务相关表使用db模式时需要建一个seata库然后执行三张表分别是global_table、branch_table、lock_table。这三张表文件一般在官方release包自带的script/server/db/mysql.sql里不用自己手敲。global_table记录全局事务状态branch_table记录每个分支事务lock_table记录全局锁占用的行。后面排查全局事务卡住时看这三张表的数据量和水位非常直观。客户端每个业务库则必须建undo_log表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, ext varchar(100) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid,branch_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8;这里最容易漏的是有几个业务库就要建几份undo_log。订单库、库存库、账户库各建各的少建一张对应分支事务执行成功后会直接报错提示找不到undo_log表。3.3 客户端依赖与application.yml配置拿一个Spring Boot Dubbo项目举例pom里需要加三个核心依赖dependency groupIdorg.apache.dubbo/groupId artifactIddubbo-spring-boot-starter/artifactId version2.7.15/version /dependency dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.8.0/version /dependency dependency groupIdio.seata/groupId artifactIdseata-dubbo/artifactId version1.8.0/version /dependency再次强调seata-dubbo不能漏。很多教程只提starter不提seata-dubbo漏掉之后全局事务依然能发起但Dubbo调用时XID不会自动透传下游分支全部脱离全局事务控制。做验证时经常出现“上游回滚了下游没回滚”的诡异现象十有八九是这个包没引。application.yml里的完整配置seata: enabled: true tx-service-group: my_test_tx_group enable-auto-data-source-proxy: true application-id: order-service registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP cluster: default username: password: service: vgroup-mapping: my_test_tx_group: defaulttx-service-group要和Server端映射一致vgroup-mapping里的my_test_tx_group把事务分组指向clusterdefault这样客户端才知道去注册中心里找cluster为default的Server实例。如果Server端cluster写的是default客户端映射也必须是default。Nacos的namespace、group、cluster三个字段两端要完全一致这几个字段只要有一个对不上客户端就找不到服务排查起来很费时间。3.4 数据源代理的两种写法AT模式的关键在于框架要能拦截到业务SQL并在执行前自动生成镜像、执行后写undo_log。这个过程依赖数据源代理没有代理Seata就拿不到SQL执行的机会。老版本写法是手动包一层Configuration public class DataSourceConfig { Bean Primary public DataSource dataSource(DataSource dataSource) { return new DataSourceProxy(dataSource); } }新版本seata-spring-boot-starter支持自动代理默认开启对应application.yml里的enable-auto-data-source-proxy: true。它会在容器启动时自动包装所有数据源。但如果项目里有多数据源、动态数据源或者用了一些第三方连接池定制逻辑自动代理经常会有漏网之鱼。一个简单排查方法在发生分支事务的Mapper执行日志里如果看到SQL之前先执行了查询前镜像的select操作说明代理生效了如果执行完业务SQL直接提交没有任何镜像查询说明代理没包上。还有个容易忽略的点如果项目同时引入dynamic-datasource这类动态数据源组件务必确认代理顺序让Seata的代理在最外层生效。否则业务代码切数据源之后真正执行的其实是原数据源Seata拦不到SQL事务照样失效。这个问题开发环境数据量小很难发现生产出问题才追悔莫及。3.5 入口方法上的GlobalTransactional在所有参与全局事务的客户端都配置好之后业务侧要做的就是在事务发起方的方法上加上注解GlobalTransactional(rollbackFor Exception.class) public void createOrder(OrderRequest request) { // 本地订单数据写入 // Dubbo调用库存服务 // Dubbo调用账户服务 }关键点有三个。第一rollbackFor一定写成Exception.class默认只在RuntimeException下回滚遇到受检异常会静默提交账单就乱了。第二注解加在事务发起方的Service方法上不是加在Dubbo接口实现上也不是加在Controller上。第三一个全局事务只有一个入口入口里调下游方法时不要再加GlobalTransactional嵌套使用会引发事务边界混乱。如果下游确实需要加锁保护但不想纳入全局事务用GlobalLock。还有一个容易被忽略的点入口方法里如果同时开启了本地Spring事务要确保GlobalTransactional在最外层。Seata会协调内层本地事务和分支的提交关系本地事务先提交分支调用后若失败Seata会根据undo_log回滚所以本地提交是允许的。但如果两层事务边界设计得特别绕还是以“全局注解在最外层Spring事务注解在业务方法上”为准则比较稳。4. AT模式运作机制拆解全局锁、undo_log与两阶段4.1 一阶段业务SQL和undo_log一起提交理解一阶段是理解AT模式的基础因为后面所有问题排查都离不开它。AT模式的第一步是拦截全部业务SQL。Seata内部的SQL解析器会先识别出SQL类型比如insert、update、delete、select for update然后从语法树上提取出主键条件和被变更的字段。以update account set balance balance - 100 where id 1为例解析器能拿到表名account、主键id1、更新字段balance。接下来会依次执行几个动作。先按主键查一次当前数据得到前镜像也就是balance1000那一行完整记录。然后执行真正的update语句。执行后再按同一个主键查一次得到后镜像balance900这一行。最后把前镜像、后镜像、SQL类型、涉及的表名等包装成一条undo_log插入到当前业务库的undo_log表。业务表和undo_log在同一个本地事务里一起提交。这五个动作里最后一个动作是整个设计成立的前提。因为业务数据和undo_log在同一本地事务里提交要么都成功要么都失败。这样二阶段回滚时只要找到undo_log就说明业务数据一定已经提交了可以安全地做反向补偿。如果业务数据提交了而undo_log没写进去回滚就无从谈起也正因为这个undo_log必须和业务表在同一个库、同一个事务里。4.2 二阶段提交是异步清理回滚是反向补偿如果全局事务的所有分支都执行成功事务管理器会通知事务协调器提交全局事务。此时各个分支的二阶段提交动作其实很轻把对应的undo_log删除、释放全局锁。因为业务数据在一阶段已经提交过了二阶段不需要再动数据库这也是AT性能优于XA的主要原因。清理undo_log是异步的对业务链路几乎无感。如果任何一个分支事务失败、超时或触发回滚事务协调器会通知所有分支做回滚。这个回滚动作就复杂了。首先根据branch_id找到undo_log里的前镜像然后校验数据是否被其他事务改过。做法是查当前数据跟后镜像比对如果当前值和后镜像一致说明一阶段之后没人动过这行可以安全恢复如果不一致说明存在脏写需要人工介入。校验通过后用前镜像生成反向SQLupdate的反向仍是updateinsert的反向是deletedelete的反向是insert。最后执行反向SQL把数据恢复为一阶段之前的状态。这套反向补偿的完整流程业务侧完全无感。业务代码不需要写任何恢复逻辑这也是AT最吸引人的地方。但反向SQL的生成依赖主键所以AT强制要求业务表必须有主键。对没有主键的表AT直接不支持启动时就会报错。还有一点要记住反向SQL是整行恢复的如果业务表字段非常多镜像数据量和恢复成本都会升高这属于AT模式的固有成本。4.3 全局锁冲突与脏读问题AT模式在数据一致性上做了一个权衡它不解决脏读只解决脏写。原因是分布式事务在很多场景下得到最终一致即可读侧允许读到未提交的中间状态但写侧必须严格串行。这个定位在选型时要想清楚如果你的业务对读一致性要求极高AT模式可能不适合。Seata通过全局锁来约束写。每个全局事务在修改某一行记录之前会先在lock_table里登记这行的全局锁。其他全局事务要改同一行发现锁被占只能等待。默认的锁重试是30次每次间隔10毫秒超过就抛“Global lock acquire timeout”。这个机制保证了不同全局事务之间不会出现脏写但也带来了明显的性能问题同一行记录在同一时间段只能被一个全局事务处理热点数据并发越高等待越长。实际调优时可以调大锁重试次数和间隔比如把client.rm.lock.retry-times改成60retry-interval改成100。但这只是缓解不是根治。真正高并发写入热点应该从业务上分流或者换方案。我用过一个粗暴有效的办法热点扣减操作改成队列串行化把并发从数据库层挪到应用层AT只负责非热点链路的兜底效果比单纯调锁参数好得多。5. 完整案例Dubbo三服务链路改造实录5.1 案例场景设计用三个服务模拟一条典型的电商下单链路order-service负责创建订单inventory-service负责扣减库存account-service负责扣减余额。三个服务各自独立数据库通过Dubbo互相调用。下单时要完成三步操作本地插入订单、远程扣库存、远程扣余额。任意一步失败全局回滚订单记录删除库存和余额恢复。参与方是三个独立的Spring Boot工程共用一个Nacos各自数据库里都需要建undo_log表。这里的表结构不复杂核心是演示AT模式下三个服务之间的事务边界如何串联。实际项目里表字段会比这多得多但改造逻辑是一样的。5.2 订单服务改造入口方法的改动订单服务是全局事务的发起方也是TM所在的位置。核心方法如下Service public class OrderServiceImpl implements OrderService { org.apache.dubbo.config.annotation.Service(version 1.0.0) GlobalTransactional(rollbackFor Exception.class) Transactional(rollbackFor Exception.class) public Order createOrder(OrderRequest request) { Order order new Order(); order.setUserId(request.getUserId()); order.setProductId(request.getProductId()); order.setAmount(request.getAmount()); orderMapper.insert(order); // 远程扣减库存 inventoryService.deduct(request.getProductId(), request.getCount()); // 远程扣减余额 accountService.deduct(request.getUserId(), request.getAmount()); return order; } org.apache.dubbo.config.annotation.Reference(version 1.0.0, timeout 5000) private InventoryService inventoryService; org.apache.dubbo.config.annotation.Reference(version 1.0.0, timeout 5000) private AccountService accountService; }这里有一个我自己踩过的坑Dubbo的Service注解和Spring的Service注解完全同名如果import写错Dubbo服务根本不会注册到注册中心调用方直接报No provider。示例里org.apache.dubbo.config.annotation.Service对应Dubbo 2.7的注解如果是Dubbo 3.x则用DubboService。两套注解混用时特别注意。下单流程中orderMapper.insert是本地事务操作库存、账户是远程Dubbo调用。全局事务注解放在最外层下面再套Spring的Transactional也没问题但一定要保证全局注解在外层。两个Reference注入的远程服务timeout建议给到5秒以上因为全局事务的锁等待时间本身就可能超过默认的1秒RPC超时这点在调整锁参数时尤其要注意。5.3 库存服务与账户服务的配套改造库存服务和账户服务都属于RM也就是资源管理器。它们本身不发起全局事务只需要保证业务SQL能被Seata代理拦截所以改造点集中在数据源代理和配置上业务方法本身基本不动。库存服务Service public class InventoryServiceImpl implements InventoryService { org.apache.dubbo.config.annotation.Service(version 1.0.0, timeout 5000) public void deduct(Long productId, Integer count) { int affected inventoryMapper.deduct(productId, count); if (affected 0) { throw new BusinessException(库存不足); } } }账户服务Service public class AccountServiceImpl implements AccountService { org.apache.dubbo.config.annotation.Service(version 1.0.0, timeout 5000) public void deduct(Long userId, BigDecimal amount) { int affected accountMapper.deduct(userId, amount); if (affected 0) { throw new BusinessException(余额不足); } } }两个服务里同样要配好application.yml里的seata配置以及数据源代理。库存Mapper和账户Mapper都是普通的update语句UPDATE inventory SET stock stock - #{count} WHERE product_id #{productId} AND stock #{count} UPDATE account SET balance balance - #{amount} WHERE user_id #{userId} AND balance #{amount}这两个update加入了库存或余额充足的条件返回affected 0就说明不够主动抛异常触发全局回滚。这是很常见的防超卖写法也是AT模式下回滚能正常工作的前提。一定要让异常传播出来别在catch里吞掉。我在一些项目里看到过catch(Exception e)之后只打日志不抛异常的情况结果全局事务永远不触发回滚账照样错。异常不往外抛Seata再强大也感知不到。5.4 验证全局事务生效的两种方式部署启动三个服务后用两个方法验证。第一种是看日志。订单服务发起全局事务时日志会出现“Begin new global transaction [xid:xxx]”。库存、账户服务执行分支事务时会出现分支事务注册成功的日志。如果只有下游日志、没有全局事务ID说明XID没有透传检查seata-dubbo依赖和Filter是否生效。第二种是做注入异常。在账户服务的deduct方法里临时加一行if(true) throw new RuntimeException(测试回滚)然后调用下单接口。正常情况下订单记录会被回滚删除库存和余额保持不变。再去库里查订单表发现下单失败后没有脏数据链路就是通的。测试完把异常代码删掉。这个注入测试我建议每个服务都要做一遍特别是第一次接入AT时。它能同时验证数据源代理、undo_log、XID透传、二阶段回滚四件事比单纯看日志可信得多。我见过太多团队只看日志里出现了XID就认为配置成功了结果注入异常测试一跑下游分支根本没回滚问题一大堆。6. 问题排查与避坑清单6.1 高频问题速查表根据实际踩坑经验我把高频问题整理成一张速查表遇到问题先对号入座现象可能原因处理方向日志一直提示no available server to connect事务分组映射错了或Server没注册到对应cluster检查vgroup-mapping与Server的cluster/namespace/group分支事务执行成功但全局不感知seata-dubbo依赖缺失XID没透传引入seata-dubbo检查Dubbo Filter是否加载下单失败后数据没回滚GlobalTransactional里的rollbackFor问题或异常被catch吞掉检查rollbackFor和异常传播分支注册报分支异常Server与client版本不一致统一两端版本提示undo_log表不存在业务库没建undo_log每个业务库建一份undo_log提示Global lock acquire timeout热点行并发冲突或锁等待时间太短调整锁重试参数热点场景重新评估方案二阶段回滚时报UndoLogException数据被其他事务改写后镜像比对不通过分析脏写来源检查事务边界设计数据源代理无效镜像SQL没有出现自动代理失效或手动代理被覆盖手动包DataSourceProxy并确保Primary这只是一个速查索引遇到具体问题还是要把异常堆栈和日志完整保留下来再排查。下面展开讲三个我实际处理过且最容易复发的坑。6.2 坑一XID透传失效导致下游没回滚某项目接入Seata后下单链路中库存扣减成功但订单服务在账户扣减时抛了异常。全局回滚时订单数据成功删除了库存却恢复不了数据库里库存已经是扣减后的值。从现象看库存服务的分支事务完全没有参与回滚。排查过程分两步。第一步看库存服务日志发现account-service的异常根本没有传递到inventory-service的全局事务上下文里。第二步看库存服务的MyBatis执行日志发现只有一条update语句没有出现任何镜像查询SQL。结合这两个现象基本能判断业务代码虽然执行了但Seata的RM没有介入。进一步查看依赖树发现工程里只有seata-spring-boot-starter没有seata-dubbo。查询源码后确认没有seata-dubbo包就不会自动注册XID透传的Dubbo Filter。加上依赖后库存服务日志开始出现分支事务注册成功的记录再跑注入异常测试库存正常回滚。这类问题的排查思路就是先问两句话有没有走代理有没有拿XID两个前提缺一个AT就会退化成普通本地事务。6.3 坑二版本不一致导致分支注册失败Seata Server用了1.6.1客户端starter用了1.7.0看起来只是小版本差异实际上双方通信使用的协议和部分消息字段有差异。现象是全局事务一发起下游执行完业务SQL后注册分支时报“Can not register branch because of config error”。这个错误容易误导人以为配置中心没配好实际上就是协议版本不匹配。混用版本还会出现另一种隐性现象下游分支事务注册成功了但一阶段等待提交时超时全局事务被标记为失败。原因是不同版本的客户端和服务端对分支状态上报的通道不同部分场景下状态没有及时同步。这个坑的解法很朴素把Server和Client版本严格对齐升级的时候也要两端一起升别只升一端。6.4 坑三数据源代理失效回滚时干瞪眼另一个项目内部自定义了一个切面会在Service方法执行前后对数据库连接做一些统计用ThreadLocal把Connection缓存起来。接入Seata后这个自定义切面拿到的Connection是原始的绕过代理直接执行SQL。结果是业务数据确实写入了但Seata完全没感知更不会写undo_log。回滚时找不到任何镜像事务直接失败。这个问题的本质是线程内Connection被提前获取并复用导致数据源代理只代理了入口实际执行SQL的Connection绕过了代理。解决办法是避免在Seata代理之外缓存Connection所有获取连接的动作都必须经过代理对象。排查时最典型的现象是分支事务日志里没有开始前镜像查询的日志也没有undo_log插入记录只有业务SQL本身。这种问题光看业务代码不一定看得出来最好的做法是在开发环境用debug日志或断点确认每次获取连接都经过DataSourceProxy。7. 一点个人体会把这套东西完整跑通之后我最大的体会是AT模式真正的难点不在原理而在配置链路和边界意识。配置链路涉及Seata Server、注册中心、事务分组、数据源代理、XID透传五个环节任何一个环节悄悄失效表面都不报错直到业务出问题才会暴露。所以每次改动配置或升级版本都一定要重复做注入回滚测试这是成本最低的保障。另外不要被“自动”两个字迷惑。AT模式自动完成的是镜像和补偿但事务边界、隔离级别、热点处理这些设计决策还是要人来定。把AT当作普通本地事务的替代品到处乱加注解只会换来更多锁冲突和脏数据。先评估业务再选方案最后才是动手集成。这套流程走下来你的分布式事务才真正落得了地。