做后端开发的这些年,遇到分布式事务的场合不算少。只要你的系统拆成了多个服务、多个数据库,面对一笔跨库写操作,就一定会碰到一致性问题。面试的时候被问“分布式事务怎么解决”,很多人第一反应就是Seata的AT模式。确实,AT模式是目前落地成本最低、业务侵入最小的一种分布式事务方案,尤其是在Spring Cloud、Dubbo这类微服务架构里,几乎成了默认选项。今天我想从自己的实践经验出发,把这个模式讲透:它的核心原理是什么,怎么一步步接入,哪些坑是我真实踩过的,以及它在什么场景下会失效。1. 为什么分布式事务绕不开Seata AT模式1.1 先搞清楚分布式事务在解决什么问题在单库单应用时代,事务非常简单:一个@Transactional包住所有数据库操作,要么全部成功,要么全部回滚。可微服务化之后,订单在订单库,库存再独立的库存库,用户在用户库。一个“下单”动作要同时扣库存、写订单、加积分,涉及三个数据库、三套连接。此时单独靠本地事务,根本无法保证一致性——订单写成功了,库存扣减失败,钱收了但货发不出去,这就是分布式事务必须解决的问题。分布式事务的目标,是让多个参与者(数据库、消息队列、缓存等)的操作达到“最终一致”。实现思路大体分成两派:一派是强同步提交,比如XA协议,通过数据库层面对资源进行两阶段锁定;另一派是补偿协调,比如TCC、SAGA、消息事务,靠业务代码来协调各个节点。Seata的AT模式,本质上属于“自动化补偿”的思路,但它又不像TCC那样要求你手写三个接口,而是用拦截器和数据源代理,把补偿逻辑自动生成出来。1.2 AT模式与XA、TCC、BASE的定位差异很多人容易把AT模式想成一种“万金油”。其实它的适用范围很明确:适合数据库事务,适合跨服务调用链路中每一步都是关系型数据库的场景。我们把它和XA、TCC放在一起对比,就能看出它的位置。维度XA协议TCCSeata AT模式侵入性数据库层面支持,业务代码无感知需要实现Try、Confirm、Cancel三个方法依赖注解和代理数据源,业务代码基本无感一致性强一致,同步阻塞最终一致,靠补偿最终一致,自动补偿性能锁持有时间长,并发低无全局锁,但开发成本高有全局锁,性能介于两者之间接入成本依赖数据库XA,配置复杂业务改造量大,逻辑容易漏建表+加依赖+加注解,几乎可快速接入典型场景单库或少量库、一致性要求极高并发高、业务明确可拆分大多数跨库微服务场景,中小并发足够从这张表能看出,AT模式赢在“性价比”。它不需要你为每个业务写补偿逻辑,也不要求数据库支持XA。代价是它引入了全局锁,在极端高并发下会有锁竞争。后续我会专门讲这个性能边界。2. AT模式核心原理:一个对业务几乎无侵入的补偿方案2.1 全局事务与分支事务的协作模型要理解AT模式,首先要建立两个概念:全局事务和分支事务。一次分布式调用链,比如“下单 -> 扣库存 -> 加积分”,整体是一个全局事务。全局事务有唯一编号XID,由Seata的事务协调器(TC)生成。而每个参与数据库操作的本地事务,都叫分支事务。分支事务由业务所处的应用进程(TM/RM)注册到TC,并绑定同一个XID。AT模式最核心的设计是:每个分支事务按照“本地事务+undo_log记录”的方式执行。也就是说,在AT模式里,并不是用数据库XA接口去预占资源,而是在每个参与库上执行的是一次普通本地事务。只不过这个本地事务除了你要执行的业务SQL,还额外写了一条undo_log日志,用于后续回滚。这样就把一个复杂的两阶段提交问题,拆分成了“在各节点各自提交,失败时统一逆向补偿”。2.2 两阶段提交在AT模式里是怎么落地的一阶段提交,大家应该听说过。在AT模式里,一阶段不是立即提交业务数据,而是把业务SQL放进一个本地事务里执行,同时把数据变更前后快照也存到undo_log表,然后直接提交这个本地事务。提交后数据已经对外可见了吗?从数据库层面看,是的,业务数据已经变了。但从全局一致性角度看,还在可控范围内,因为TC上记录了这个全局事务持有的全局锁,其他AT全局事务如果也要修改这些行,会被阻塞。二阶段则分成两种情况:全局事务成功时,TC通知所有分支参与者删除undo_log,释放全局锁,整个流程就算完成;全局业务抛异常时,TC通知各分支发起回滚,每个分支读到自己的undo_log,根据before image构造“反向SQL”把数据改回去,这就是回滚逻辑。整个过程业务方无需手写补偿,Seata的RM(资源管理器)在底层完成了。2.3 数据快照与undo log:回滚的关键我对一个刚接触Seata的朋友说过:你把undo_log理解成数据库的“后悔药”就行。每一次业务数据被修改前,AT模式会先查询这条数据原样,存成before image;执行完业务SQL之后,再把修改后的数据查出来,存成after image。这两份镜像连同分支事务ID、全局XID一起,通过代理数据源写入同一个本地事务里。注意,写入undo_log和执行业务SQL必须在同一个数据库事务内,否则日志与数据不同步,后面回滚就乱了。回滚时,Seata会做一次“防脏写校验”:把当前数据库里的数据与after image对比。如果一致,说明这个数据没有被其他事务改过,可以安全地用before image生成反向UPDATE或DELETE语句恢复数据;如果不一致,说明产生了脏写,Seata会放弃自动回滚,并把该分支事务标记为log_status=1,提示人工介入。这个机制看起来简单,但实际生产环境一旦出现脏写,处理起来非常痛苦,后文我会结合故障案例展开。-- undo_log 表结构(MySQL版本,不同数据库略有差异) CREATE TABLE IF NOT EXISTS `undo_log` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id', `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:unable to rollback', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';2.4 全局锁与写隔离:并发安全的基础AT模式默认的隔离级别是“读未提交”。这听起来有点可怕,但结合全局锁之后,情况并没那么糟糕。Seata引入了一把逻辑上的全局锁:当一个分支事务修改某一行数据时,它会在TC上注册该行的全局锁。其他全局事务如果也要修改同一行,必须先尝试获取这把锁,获取不到就进入等待或重试。这样能保证不同全局事务之间不会互相覆盖数据,这就是写隔离。但要注意,这个全局锁是逻辑锁,不依赖数据库的行锁。如果一个非Seata管理的普通事务直接修改了同一行,AT模式的全局锁根本拦不住。因此在接入AT模式时,要严格要求当前数据表只被Seata管理的应用访问,或者至少所有写操作都走同一个微服务。否则一旦出现“旁路更新”,脏写概率极高,回滚时就会丢数据。3. 从0到1接入Seata AT模式:实操步骤与配置要点3.1 部署TC并初始化全局事务会话表第一步是准备好TC,也就是Seata Server。我推荐使用1.5.0以上版本,注册中心可以选Nacos,也可以用文件配置。这里不展开部署细节,只提醒几个关键点:TC侧需要一个可靠的存储,默认支持文件、数据库、Redis三种模式。生产环境建议使用数据库模式,把全局事务会话持久化到global_table、branch_table和lock_table三张表里,避免TC重启后全局事务信息丢失。启动前还需要调整内存参数,因为TC会缓存全局锁和事务会话。小项目默认配置能跑,但并发稍微上来后,堆内存得给足,我习惯至少给到1G。另外要注意TC与业务服务的时间同步,虽然Seata没有强制要求严格对时,但超时判断依赖时间戳,偏差太大会导致误判超时回滚。3.2 引入依赖并配置数据源代理客户端接入时,如果是Spring Boot项目,直接加seata-spring-boot-starter。这里的核心是数据源代理。AT模式之所以能自动解析SQL、生成undo_log,前提是数据源被Seata的DataSourceProxy包装了一层。所以配置里要确保Seata能够拿到你的DataSource,而不是直接操作业务数据源。以目前主流版本为例,yml配置大致如下:seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group service: vgroup-mapping: my_test_tx_group: default grouplist: default: 127.0.0.1:8091 registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: public group: SEATA_GROUP username: nacos password: nacos一个很容易踩的坑:如果项目中同时使用MyBatis、多数据源、动态数据源,一定要保证Seata代理的是最终路由到的数据源。尤其使用dynamic-datasource时,需要给每个业务数据源单独配置代理,或者使用它提供的Seata集成包。否则事务管理器和AT分支注册会拿到不一致的DataSource,导致分支事务无法生效。3.3 在业务代码中开启全局事务在调用链的最顶层方法上,也就是整个分布式事务的发起者,加上@GlobalTransactional注解。典型例子是订单服务调用库存服务和积分服务,那么订单服务里的createOrder方法就是全局事务的入口。@Service public class OrderService { @GlobalTransactional(rollbackFor = Exception.class) public void createOrder(OrderCreateDTO dto) { orderMapper.insert(dto.toOrder()); stockService.deductStock(dto.getProductId(), dto.getQuantity()); pointService.addPoint(dto.getUserId(), dto.getAmount()); } }注意rollbackFor一定要配,否则默认只回滚RuntimeException,很多业务里抛的自定义异常是Exception子类,不配就会导致事务“假成功”。另外,被调用的服务方法本身最好不要再加@GlobalTransactional,只需要保持正常的本地事务@Transactional即可。全局事务的XID会通过RPC框架的钩子自动传递到下游服务,如果不传递,下游就成了独立事务。3.4 验证一阶段提交与二阶段回滚接入完成之后,不要急着上线,先在测试环境做一次“破坏实验”。最简单的方法:让stockService.deductStock里故意抛一个异常,然后观察数据库。正常情况下,订单表里的数据会被插入后又被回滚删除,库存表也不会变化。同时查看undo_log表,回滚成功的记录通常已经被清理。再验证成功路径,看看undo_log里是否残留日志。理论上二阶段提交后日志会被删除,但如果看到大量残留,可能是清理失败的信号。常见原因包括:TC异常、网络中断、客户端版本与TC版本不兼容。日志残留不影响业务数据,但会持续占用存储空间,并且这些历史记录无法区分哪些是已提交的,长期下来会误导排查,建议定时清理。4. 一次真实的回滚故障排查:AT模式实战避坑4.1 问题现象:全局事务回滚了,但数据没恢复有一次在某个模拟订单系统里,库存服务超时,全局事务按预期回滚。下游检查时发现库存数据被改了:原本扣减100件,回滚后库存数量少了50件。更奇怪的是,undo_log里该分支记录已经被标记为log_status=1,Seata的控制台也给出了“脏数据无法自动回滚”的提示。这个现象的根本原因在于,扣减库存的执行流程里,业务代码在调用库存扣减前,先做了一次状态更新,然后又有一个后台的普通事务去修改了同一行的库存数量。AT模式的全局锁没有拦住这个后台任务,因为它不在全局事务上下文里,也没有通过Seata代理的数据源。于是回滚时,当前数据与after image不一致,Seata判定为脏写,拒绝执行自动补偿。4.2 排查思路:从undo log到脏写源头那天排查过程很有意思。我先看了全局事务日志,发现TC上分支回滚失败的原因确实是Client RM rollback failed。然后对比库里当前数据和undo_log中after image的二进制内容,确认数据已经被第三者修改。接着查后台任务,发现是一个定时对账任务,它在同一张表上做了“先将库存调整为可用数量,再扣减预占数量”的两步操作,中间没有任何分布式事务控制。定位到根因后,我立刻给该表的所有写操作加了访问规约:要么全部走统一库存服务,要么业务侧接入Seata代理,禁止出现“旁路更新”。同时把update语句改为条件更新,例如update stock set available = available - ? where product_id = ? and available >= ?,让并发问题的暴露更早,而不是等回滚阶段才发现脏写。这一步很关键,因为AT模式的补偿能力再强,也无法处理已经污染的现场。4.3 常见问题速查表:脏读、全局锁失效、超时下面这张表是我在维护AT模式项目期间整理的高频问题,基本覆盖了日常90%的坑。现象可能原因解决思路全局事务一直超时回滚分支事务执行时间超过timeout调大client.rm.timeout或全局事务超时参数下游服务报xid not foundXID传递链路断开,比如用了异步线程或消息队列检查RPC透传配置;异步分支改用独立事务或消息事务回滚时出现脏写提示存在非Seata管理的事务修改同一行禁止旁路更新;改为条件更新并用分布式锁兜底本地事务无法启动未正确代理DataSource检查seata.enabled和数据源配置undo_log表不断增大二阶段提交清理失败或回滚失败排查TC连接、客户端版本;定时清理确认已完成的日志批量更新性能骤降大范围更新导致全局锁竞争严重拆分批次,减少单行锁持有时间这里面最容易被忽略的是全局锁失效。我见过一个项目,全局事务范围非常大,一次要处理上千行数据,每个分支持有锁的时间超过几秒,并发情况下其他全局事务全部卡住,紧接着一批超时回滚,反而导致了更严重的不一致。所以AT模式对事务范围的敏感度,比想象中高得多。5. AT模式会遇到的性能边界与调优经验5.1 全局锁竞争对吞吐量的影响AT模式引入的全局锁,本质上是一个集中式的并发控制点,这个设计一定会带来性能损耗。当两个全局事务同时操作一行数据时,后一个分支要等前一个分支完成二阶段释放锁后才能继续。如果前一个全局事务链路很长(比如调用三个服务,每步都做网络IO),锁占用时间就会很长。我用一个简单例子估算过:假设一行库存数据的全局锁平均持有时间50毫秒,那么同一行数据的理论并发上限就是1秒/0.05秒=20TPS。如果业务要求100TPS,那么AT模式直接就不够用。这时候要么放弃对同一行的实时热点操作,把库存预分配到多个子库存,要么换TCC模式,把锁定粒度放到业务上:例如在Try阶段不真正扣减,而是冻结部分库存,Confirm阶段再落库。提前算清楚这个瓶颈,比上线后加机器更有意义。5.2 减少无谓的分布式事务范围很多团队接入AT模式后,喜欢把一个大方法整体包上@GlobalTransactional,甚至把查询操作也放进去。这是典型的过度使用。全局事务的范围应该严格限制在“必须保证一致性的写操作链路”。查询不需要参与分布式事务,因为它是读取可能已提交的快照,不会造成数据不一致。还有一类场景不适合AT:长期占用外部资源,比如在全局事务内调用第三方支付接口并等待回调。这会拉长全局锁持有时间,让整张表都跟着卡住。我建议的做法是:先执行本地数据库操作并提交,再异步发送支付消息,通过消息回调更新订单状态,把“支付结果”与“库存扣减”解耦。如果实在要强一致,也要把外部调用放到事务的最后,并缩短超时时间,减少锁占用。5.3 连接池与事务超时参数调整AT模式每一阶段都会额外查询前镜像、后镜像,这意味着相比普通SQL,每条被代理的SQL至少多出两次查询。如果业务表数据量大,或者SQL没有索引,这个额外开销会被放大。所以接入AT模式前,最好给所有会被代理更新的表加上合适的索引。否则一阶段生成before image时,全表扫描会让数据库压力瞬间飙高。连接池方面,DataSourceProxy和底层实际数据源是两层连接,连接池大小配置要考虑全局事务内部的多数据源调用。常规情况下,把最大连接数调大20%~30%是合理的。超时参数也需要联动:timeout控制全局事务超时,默认60秒;client.rm.reportRetryCount控制分支注册重试次数;client.tm.commitRetryCount和rollbackRetryCount控制TC通知提交/回滚的重试次数。在弱网环境下,重试次数过低会遇到“回滚没生效”的假象,过高又可能影响性能,我一般把回滚重试设为5次,提交重试设为3次。还有一个容易被忽略的点:批量操作SQL的解析支持。AT模式对SQL语法有解析限制,像INSERT INTO ... ON DUPLICATE KEY UPDATE、REPLACE INTO这类写法,在某些版本中可能解析异常或生成错误镜像。生产代码里尽量避免这种写法,或者改成先查询再逐条更新。数据库字段方面,TEXT、BLOB这类大字段虽然能放入undo_log,但序列化开销很大,如果表里必须有大字段,最好拆表或用冗余字段,避免纳入全局事务的更新范围。从我自己实际维护过的几个项目来看,Seata AT模式非常适合作“从单体拆成微服务”的过渡期数据一致性方案。它不需要大规模改业务代码,能让团队快速落地最终一致。但它的天花板也很明显:全局锁的集中式调度决定了它不适合超高并发热点行更新,也不适合非数据库资源的事务协调。我个人通常的建议是:中小规模并发、跨库写操作,直接上AT模式,省心可靠;高并发热点写、需要极致性能的场景,再去研究TCC或SAGA。理解了这个模式的边界,你才不会在压测的时候对着监控图一筹莫展。最后提一个实用技巧:日常运维时,多做一次“模拟回滚”的演练,让开发同学清楚看到脏写发生在哪一步,这样等真出问题的时候,大家心理都有底。
做后端开发的这些年,遇到分布式事务的场合不算少。只要你的系统拆成了多个服务、多个数据库,面对一笔跨库写操作,就一定会碰到一致性问题。面试的时候被问“分布式事务怎么解决”,很多人第一反应就是Seata的AT模式。确实,AT模式是目前落地成本最低、业务侵入最小的一种分布式事务方案,尤其是在Spring Cloud、Dubbo这类微服务架构里,几乎成了默认选项。今天我想从自己的实践经验出发,把这个模式讲透:它的核心原理是什么,怎么一步步接入,哪些坑是我真实踩过的,以及它在什么场景下会失效。1. 为什么分布式事务绕不开Seata AT模式1.1 先搞清楚分布式事务在解决什么问题在单库单应用时代,事务非常简单:一个@Transactional包住所有数据库操作,要么全部成功,要么全部回滚。可微服务化之后,订单在订单库,库存再独立的库存库,用户在用户库。一个“下单”动作要同时扣库存、写订单、加积分,涉及三个数据库、三套连接。此时单独靠本地事务,根本无法保证一致性——订单写成功了,库存扣减失败,钱收了但货发不出去,这就是分布式事务必须解决的问题。分布式事务的目标,是让多个参与者(数据库、消息队列、缓存等)的操作达到“最终一致”。实现思路大体分成两派:一派是强同步提交,比如XA协议,通过数据库层面对资源进行两阶段锁定;另一派是补偿协调,比如TCC、SAGA、消息事务,靠业务代码来协调各个节点。Seata的AT模式,本质上属于“自动化补偿”的思路,但它又不像TCC那样要求你手写三个接口,而是用拦截器和数据源代理,把补偿逻辑自动生成出来。1.2 AT模式与XA、TCC、BASE的定位差异很多人容易把AT模式想成一种“万金油”。其实它的适用范围很明确:适合数据库事务,适合跨服务调用链路中每一步都是关系型数据库的场景。我们把它和XA、TCC放在一起对比,就能看出它的位置。维度XA协议TCCSeata AT模式侵入性数据库层面支持,业务代码无感知需要实现Try、Confirm、Cancel三个方法依赖注解和代理数据源,业务代码基本无感一致性强一致,同步阻塞最终一致,靠补偿最终一致,自动补偿性能锁持有时间长,并发低无全局锁,但开发成本高有全局锁,性能介于两者之间接入成本依赖数据库XA,配置复杂业务改造量大,逻辑容易漏建表+加依赖+加注解,几乎可快速接入典型场景单库或少量库、一致性要求极高并发高、业务明确可拆分大多数跨库微服务场景,中小并发足够从这张表能看出,AT模式赢在“性价比”。它不需要你为每个业务写补偿逻辑,也不要求数据库支持XA。代价是它引入了全局锁,在极端高并发下会有锁竞争。后续我会专门讲这个性能边界。2. AT模式核心原理:一个对业务几乎无侵入的补偿方案2.1 全局事务与分支事务的协作模型要理解AT模式,首先要建立两个概念:全局事务和分支事务。一次分布式调用链,比如“下单 -> 扣库存 -> 加积分”,整体是一个全局事务。全局事务有唯一编号XID,由Seata的事务协调器(TC)生成。而每个参与数据库操作的本地事务,都叫分支事务。分支事务由业务所处的应用进程(TM/RM)注册到TC,并绑定同一个XID。AT模式最核心的设计是:每个分支事务按照“本地事务+undo_log记录”的方式执行。也就是说,在AT模式里,并不是用数据库XA接口去预占资源,而是在每个参与库上执行的是一次普通本地事务。只不过这个本地事务除了你要执行的业务SQL,还额外写了一条undo_log日志,用于后续回滚。这样就把一个复杂的两阶段提交问题,拆分成了“在各节点各自提交,失败时统一逆向补偿”。2.2 两阶段提交在AT模式里是怎么落地的一阶段提交,大家应该听说过。在AT模式里,一阶段不是立即提交业务数据,而是把业务SQL放进一个本地事务里执行,同时把数据变更前后快照也存到undo_log表,然后直接提交这个本地事务。提交后数据已经对外可见了吗?从数据库层面看,是的,业务数据已经变了。但从全局一致性角度看,还在可控范围内,因为TC上记录了这个全局事务持有的全局锁,其他AT全局事务如果也要修改这些行,会被阻塞。二阶段则分成两种情况:全局事务成功时,TC通知所有分支参与者删除undo_log,释放全局锁,整个流程就算完成;全局业务抛异常时,TC通知各分支发起回滚,每个分支读到自己的undo_log,根据before image构造“反向SQL”把数据改回去,这就是回滚逻辑。整个过程业务方无需手写补偿,Seata的RM(资源管理器)在底层完成了。2.3 数据快照与undo log:回滚的关键我对一个刚接触Seata的朋友说过:你把undo_log理解成数据库的“后悔药”就行。每一次业务数据被修改前,AT模式会先查询这条数据原样,存成before image;执行完业务SQL之后,再把修改后的数据查出来,存成after image。这两份镜像连同分支事务ID、全局XID一起,通过代理数据源写入同一个本地事务里。注意,写入undo_log和执行业务SQL必须在同一个数据库事务内,否则日志与数据不同步,后面回滚就乱了。回滚时,Seata会做一次“防脏写校验”:把当前数据库里的数据与after image对比。如果一致,说明这个数据没有被其他事务改过,可以安全地用before image生成反向UPDATE或DELETE语句恢复数据;如果不一致,说明产生了脏写,Seata会放弃自动回滚,并把该分支事务标记为log_status=1,提示人工介入。这个机制看起来简单,但实际生产环境一旦出现脏写,处理起来非常痛苦,后文我会结合故障案例展开。-- undo_log 表结构(MySQL版本,不同数据库略有差异) CREATE TABLE IF NOT EXISTS `undo_log` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id', `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:unable to rollback', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';2.4 全局锁与写隔离:并发安全的基础AT模式默认的隔离级别是“读未提交”。这听起来有点可怕,但结合全局锁之后,情况并没那么糟糕。Seata引入了一把逻辑上的全局锁:当一个分支事务修改某一行数据时,它会在TC上注册该行的全局锁。其他全局事务如果也要修改同一行,必须先尝试获取这把锁,获取不到就进入等待或重试。这样能保证不同全局事务之间不会互相覆盖数据,这就是写隔离。但要注意,这个全局锁是逻辑锁,不依赖数据库的行锁。如果一个非Seata管理的普通事务直接修改了同一行,AT模式的全局锁根本拦不住。因此在接入AT模式时,要严格要求当前数据表只被Seata管理的应用访问,或者至少所有写操作都走同一个微服务。否则一旦出现“旁路更新”,脏写概率极高,回滚时就会丢数据。3. 从0到1接入Seata AT模式:实操步骤与配置要点3.1 部署TC并初始化全局事务会话表第一步是准备好TC,也就是Seata Server。我推荐使用1.5.0以上版本,注册中心可以选Nacos,也可以用文件配置。这里不展开部署细节,只提醒几个关键点:TC侧需要一个可靠的存储,默认支持文件、数据库、Redis三种模式。生产环境建议使用数据库模式,把全局事务会话持久化到global_table、branch_table和lock_table三张表里,避免TC重启后全局事务信息丢失。启动前还需要调整内存参数,因为TC会缓存全局锁和事务会话。小项目默认配置能跑,但并发稍微上来后,堆内存得给足,我习惯至少给到1G。另外要注意TC与业务服务的时间同步,虽然Seata没有强制要求严格对时,但超时判断依赖时间戳,偏差太大会导致误判超时回滚。3.2 引入依赖并配置数据源代理客户端接入时,如果是Spring Boot项目,直接加seata-spring-boot-starter。这里的核心是数据源代理。AT模式之所以能自动解析SQL、生成undo_log,前提是数据源被Seata的DataSourceProxy包装了一层。所以配置里要确保Seata能够拿到你的DataSource,而不是直接操作业务数据源。以目前主流版本为例,yml配置大致如下:seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group service: vgroup-mapping: my_test_tx_group: default grouplist: default: 127.0.0.1:8091 registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: public group: SEATA_GROUP username: nacos password: nacos一个很容易踩的坑:如果项目中同时使用MyBatis、多数据源、动态数据源,一定要保证Seata代理的是最终路由到的数据源。尤其使用dynamic-datasource时,需要给每个业务数据源单独配置代理,或者使用它提供的Seata集成包。否则事务管理器和AT分支注册会拿到不一致的DataSource,导致分支事务无法生效。3.3 在业务代码中开启全局事务在调用链的最顶层方法上,也就是整个分布式事务的发起者,加上@GlobalTransactional注解。典型例子是订单服务调用库存服务和积分服务,那么订单服务里的createOrder方法就是全局事务的入口。@Service public class OrderService { @GlobalTransactional(rollbackFor = Exception.class) public void createOrder(OrderCreateDTO dto) { orderMapper.insert(dto.toOrder()); stockService.deductStock(dto.getProductId(), dto.getQuantity()); pointService.addPoint(dto.getUserId(), dto.getAmount()); } }注意rollbackFor一定要配,否则默认只回滚RuntimeException,很多业务里抛的自定义异常是Exception子类,不配就会导致事务“假成功”。另外,被调用的服务方法本身最好不要再加@GlobalTransactional,只需要保持正常的本地事务@Transactional即可。全局事务的XID会通过RPC框架的钩子自动传递到下游服务,如果不传递,下游就成了独立事务。3.4 验证一阶段提交与二阶段回滚接入完成之后,不要急着上线,先在测试环境做一次“破坏实验”。最简单的方法:让stockService.deductStock里故意抛一个异常,然后观察数据库。正常情况下,订单表里的数据会被插入后又被回滚删除,库存表也不会变化。同时查看undo_log表,回滚成功的记录通常已经被清理。再验证成功路径,看看undo_log里是否残留日志。理论上二阶段提交后日志会被删除,但如果看到大量残留,可能是清理失败的信号。常见原因包括:TC异常、网络中断、客户端版本与TC版本不兼容。日志残留不影响业务数据,但会持续占用存储空间,并且这些历史记录无法区分哪些是已提交的,长期下来会误导排查,建议定时清理。4. 一次真实的回滚故障排查:AT模式实战避坑4.1 问题现象:全局事务回滚了,但数据没恢复有一次在某个模拟订单系统里,库存服务超时,全局事务按预期回滚。下游检查时发现库存数据被改了:原本扣减100件,回滚后库存数量少了50件。更奇怪的是,undo_log里该分支记录已经被标记为log_status=1,Seata的控制台也给出了“脏数据无法自动回滚”的提示。这个现象的根本原因在于,扣减库存的执行流程里,业务代码在调用库存扣减前,先做了一次状态更新,然后又有一个后台的普通事务去修改了同一行的库存数量。AT模式的全局锁没有拦住这个后台任务,因为它不在全局事务上下文里,也没有通过Seata代理的数据源。于是回滚时,当前数据与after image不一致,Seata判定为脏写,拒绝执行自动补偿。4.2 排查思路:从undo log到脏写源头那天排查过程很有意思。我先看了全局事务日志,发现TC上分支回滚失败的原因确实是Client RM rollback failed。然后对比库里当前数据和undo_log中after image的二进制内容,确认数据已经被第三者修改。接着查后台任务,发现是一个定时对账任务,它在同一张表上做了“先将库存调整为可用数量,再扣减预占数量”的两步操作,中间没有任何分布式事务控制。定位到根因后,我立刻给该表的所有写操作加了访问规约:要么全部走统一库存服务,要么业务侧接入Seata代理,禁止出现“旁路更新”。同时把update语句改为条件更新,例如update stock set available = available - ? where product_id = ? and available >= ?,让并发问题的暴露更早,而不是等回滚阶段才发现脏写。这一步很关键,因为AT模式的补偿能力再强,也无法处理已经污染的现场。4.3 常见问题速查表:脏读、全局锁失效、超时下面这张表是我在维护AT模式项目期间整理的高频问题,基本覆盖了日常90%的坑。现象可能原因解决思路全局事务一直超时回滚分支事务执行时间超过timeout调大client.rm.timeout或全局事务超时参数下游服务报xid not foundXID传递链路断开,比如用了异步线程或消息队列检查RPC透传配置;异步分支改用独立事务或消息事务回滚时出现脏写提示存在非Seata管理的事务修改同一行禁止旁路更新;改为条件更新并用分布式锁兜底本地事务无法启动未正确代理DataSource检查seata.enabled和数据源配置undo_log表不断增大二阶段提交清理失败或回滚失败排查TC连接、客户端版本;定时清理确认已完成的日志批量更新性能骤降大范围更新导致全局锁竞争严重拆分批次,减少单行锁持有时间这里面最容易被忽略的是全局锁失效。我见过一个项目,全局事务范围非常大,一次要处理上千行数据,每个分支持有锁的时间超过几秒,并发情况下其他全局事务全部卡住,紧接着一批超时回滚,反而导致了更严重的不一致。所以AT模式对事务范围的敏感度,比想象中高得多。5. AT模式会遇到的性能边界与调优经验5.1 全局锁竞争对吞吐量的影响AT模式引入的全局锁,本质上是一个集中式的并发控制点,这个设计一定会带来性能损耗。当两个全局事务同时操作一行数据时,后一个分支要等前一个分支完成二阶段释放锁后才能继续。如果前一个全局事务链路很长(比如调用三个服务,每步都做网络IO),锁占用时间就会很长。我用一个简单例子估算过:假设一行库存数据的全局锁平均持有时间50毫秒,那么同一行数据的理论并发上限就是1秒/0.05秒=20TPS。如果业务要求100TPS,那么AT模式直接就不够用。这时候要么放弃对同一行的实时热点操作,把库存预分配到多个子库存,要么换TCC模式,把锁定粒度放到业务上:例如在Try阶段不真正扣减,而是冻结部分库存,Confirm阶段再落库。提前算清楚这个瓶颈,比上线后加机器更有意义。5.2 减少无谓的分布式事务范围很多团队接入AT模式后,喜欢把一个大方法整体包上@GlobalTransactional,甚至把查询操作也放进去。这是典型的过度使用。全局事务的范围应该严格限制在“必须保证一致性的写操作链路”。查询不需要参与分布式事务,因为它是读取可能已提交的快照,不会造成数据不一致。还有一类场景不适合AT:长期占用外部资源,比如在全局事务内调用第三方支付接口并等待回调。这会拉长全局锁持有时间,让整张表都跟着卡住。我建议的做法是:先执行本地数据库操作并提交,再异步发送支付消息,通过消息回调更新订单状态,把“支付结果”与“库存扣减”解耦。如果实在要强一致,也要把外部调用放到事务的最后,并缩短超时时间,减少锁占用。5.3 连接池与事务超时参数调整AT模式每一阶段都会额外查询前镜像、后镜像,这意味着相比普通SQL,每条被代理的SQL至少多出两次查询。如果业务表数据量大,或者SQL没有索引,这个额外开销会被放大。所以接入AT模式前,最好给所有会被代理更新的表加上合适的索引。否则一阶段生成before image时,全表扫描会让数据库压力瞬间飙高。连接池方面,DataSourceProxy和底层实际数据源是两层连接,连接池大小配置要考虑全局事务内部的多数据源调用。常规情况下,把最大连接数调大20%~30%是合理的。超时参数也需要联动:timeout控制全局事务超时,默认60秒;client.rm.reportRetryCount控制分支注册重试次数;client.tm.commitRetryCount和rollbackRetryCount控制TC通知提交/回滚的重试次数。在弱网环境下,重试次数过低会遇到“回滚没生效”的假象,过高又可能影响性能,我一般把回滚重试设为5次,提交重试设为3次。还有一个容易被忽略的点:批量操作SQL的解析支持。AT模式对SQL语法有解析限制,像INSERT INTO ... ON DUPLICATE KEY UPDATE、REPLACE INTO这类写法,在某些版本中可能解析异常或生成错误镜像。生产代码里尽量避免这种写法,或者改成先查询再逐条更新。数据库字段方面,TEXT、BLOB这类大字段虽然能放入undo_log,但序列化开销很大,如果表里必须有大字段,最好拆表或用冗余字段,避免纳入全局事务的更新范围。从我自己实际维护过的几个项目来看,Seata AT模式非常适合作“从单体拆成微服务”的过渡期数据一致性方案。它不需要大规模改业务代码,能让团队快速落地最终一致。但它的天花板也很明显:全局锁的集中式调度决定了它不适合超高并发热点行更新,也不适合非数据库资源的事务协调。我个人通常的建议是:中小规模并发、跨库写操作,直接上AT模式,省心可靠;高并发热点写、需要极致性能的场景,再去研究TCC或SAGA。理解了这个模式的边界,你才不会在压测的时候对着监控图一筹莫展。最后提一个实用技巧:日常运维时,多做一次“模拟回滚”的演练,让开发同学清楚看到脏写发生在哪一步,这样等真出问题的时候,大家心理都有底。