ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Seata XA模式实战:订单库存跨库强一致,从原理到踩坑全解析

Seata XA模式实战:订单库存跨库强一致,从原理到踩坑全解析 如果你手头也有一个“下单成功但库存没扣”、“库存扣了但订单失败”这种跨库数据不一致的问题那你已经站在分布式事务的门槛上了。这篇文章聊的 Seata XA 模式是我在一个电商后端项目里实际落地过的方案用在两个 MySQL 库之间做订单和库存的强一致写入整体效果稳定。XA 模式最吸引人的地方在于业务代码几乎不需要关心分布式事务的细节它直接依赖数据库底层的 XA 协议让多个库像一个库一样原子提交。这篇文章会从底层原理拆起给出一份可以直接复用的配置和代码然后重点分享我在实际使用中踩过的悬挂事务、连接池耗尽、XID 丢失这些坑希望能帮你少走弯路。1. 一个下单接口两个数据库XA 模式解决的是什么层面的问题1.1 为什么“扣库存”和“下订单”不能分两次提交先还原一下典型的业务场景。订单服务和库存服务拆库拆服务已经是很常见的架构了订单库order_db里写订单库存库storage_db里扣库存。两个库各自有本地事务单看一个库数据没问题但跨库就不一样了。用户下单那一刻你是先插入订单还是先扣库存无论哪个先执行只要另一个失败业务上就出问题了。你可能会想那先扣库存失败就回滚订单不就行了问题是这两个操作在物理上属于两个独立的数据源各自的commit是即时的一旦先执行的那个提交了后执行的失败了前面的操作已经从数据库层面落盘本地事务管不到了。这不是代码 bug而是单机事务模型在分布式环境下的天然局限。最简单也最常见的“土办法”是手工补偿扣库存失败后再发一条消息把订单取消掉。这确实能解决一部分问题但它属于最终一致而且补偿逻辑是业务代码自己写的一旦补偿过程本身失败或者消息队列抖动数据照样不一致。最终一致不是不好而是要看场景。电商下单这种用户感知强、链路短的场景我更倾向于让数据库层面保证强一致而不是靠业务代码去兜底。1.2 Seata XA 在分布式事务方案里的定位以及它和 AT、TCC 的分工Seata 一共提供四种事务模式AT、TCC、SAGA、XA。很多刚接触 Seata 的人容易把 AT 当成唯一选择毕竟它用起来最简单、性能也相对好。但 AT 模式本质上是 Seata 自研的一种“补偿式”方案它会在 SQL 执行前记录前后镜像二阶段通过反向 SQL 来恢复数据。这个方案很巧妙但它有一个前提Seata 必须能解析你的 SQL如果你的 SQL 写得比较特殊或者数据库方言支持不完善AT 的补偿就可能出问题。XA 模式走的是另一条路。它不是 Seata 发明的而是 X/Open DTP 模型中定义的分布式事务处理标准Oracle、MySQL、PostgreSQL 这些主流数据库都实现了 XA 协议。Seata 所做的是把数据库原生的 XA 能力编排进自己的全局事务框架让多个跨库的 XA 事务被同一个全局事务ID管理起来。相比之下XA 模式在一致性上更“硬核”因为提交和回滚的最终裁决者是数据库本身而不是 Seata 的补偿逻辑。我的判断标准很简单如果业务对强一致要求高比如支付、下单、转账并且事务链路短、并发冲突不大XA 模式是首选如果业务链路长、并发高、要求最终一致就好那 AT、TCC、SAGA 各有各的适用场景。XA 不是万能的但对于“两个库、一次请求、要绝对一致”这种场景它是我最愿意用的方案。2. 拆开 XA 模式的底层骨架二阶段提交以及 TM/TC/RM 的博弈2.1 TC、TM、RM 三个角色对照一次下单请求串一遍Seata 整个分布式事务框架里有三个角色必须彻底理解面试也爱问TM、TC、RM。TMTransaction Manager是事务管理器负责全局事务的开启、提交和回滚业务代码里那个GlobalTransactional注解就是 TM 的入口。TCTransaction Coordinator是事务协调者也就是独立部署的 Seata Server它维护全局事务的状态给所有参与者下达提交或回滚的指令。RMResource Manager是资源管理器负责管理分支事务在 Seata 里RM 对应的是被代理的数据源它会向 TC 注册分支并执行实际的数据库操作。对照一次下单请求来看整个流程是这样的业务方法入口上有GlobalTransactionalTM 向 TC 申请开启全局事务TC 生成一个全局事务 ID也就是 XID返回给 TM。业务代码开始执行第一次访问订单库时订单库对应的 RM 向 TC 注册分支事务并执行XA START把当前数据库连接纳入 XA 事务。订单库执行插入 SQL执行XA END再执行XA PREPARE此刻订单库的事务已经进入 prepare 状态数据锁没有释放。同样地库存库执行扣减 SQL也走一遍XA START、SQL、XA END、XA PREPARE流程。所有分支都 prepare 完成TM 向 TC 发起全局提交指令TC 通知所有 RM 执行XA COMMIT。如果任何一个分支 prepare 失败TM 向 TC 发起全局回滚TC 通知所有 RM 执行XA ROLLBACK。这里最关键的一点是XA 的一阶段结束于 PREPARE而不是 COMMIT。数据库在 PREPARE 阶段已经确定了自己“可以提交”但锁不会释放一直要等到二阶段收到全局提交或者回滚指令才真正落定。这也是 XA 模式性能和并发上限不如 AT 模式的根本原因。2.2 Seata XA 和标准数据库 XA 的关系以及数据库底层的真实动作很多人把 Seata XA 理解成 Seata 自己实现了一套 XA这是不对的。Seata XA 里的 XA 动作最终都是数据库自己完成的Seata 的 RM 只是封装或者说代理了数据库的 XA 能力。你完全可以用 MySQL 客户端手动模拟一遍 XA 的执行过程这样理解会更深刻XA START global-001; -- 这里执行你的业务SQL比如 INSERT INTO t_order (order_no, commodity_code, count, amount) VALUES (SN20240101, C001, 1, 99.90); XA END global-001; XA PREPARE global-001; -- 到这里当前连接上的事务已经被数据库置于 prepare 状态但数据还没提交 XA COMMIT global-001; -- 或者 XA ROLLBACK global-001;注意这个global-001它就是 Seata 里说的 XID 的数据库形态。Seata 的 RP 在执行业务 SQL 之前会先拿到一条物理连接然后像上面一样执行XA START把你的 SQL 包在中间XA END之后执行XA PREPARE最后等待 TC 的指令做XA COMMIT或者XA ROLLBACK。所以你在代码里看到的DataSourceProxyXA它代理的不是业务逻辑而是“让这个数据源拿到的连接在执行 SQL 前自动完成 XA 事务的开启和 prepare”。这就是为什么 XA 模式下业务代码几乎不用改只需要把数据源换掉。2.3 XA 为什么能做到“强一致”但它锁住了什么强一致的核心在于 PREPARE 这个动作。在一个分布式系统里多个数据库各自执行本地事务最怕的就是“一部分提交了一部分回滚了”。XA 用两阶段提交解决这个问题第一阶段先让所有参与者表态我是否可以提交第二阶段根据所有人的表态统一决定是提交还是回滚。这种设计的巧妙之处在于任何一个数据库只要进入了 PREPARE 状态它就保证“只要收到 COMMIT 指令就一定能提交成功”不会再因为本地问题反悔。因为真正可能出错的地方比如唯一键冲突、约束失败、磁盘空间不足都已经在第一阶段暴露了。所以 TC 在第二阶段只需要做简单的指令转发不会出现“说好了提交结果提交失败”的尴尬情况。代价就是把锁拉长了。在 MySQL 的 InnoDB 里一个事务从 PREPARE 到最终全局 COMMIT/ROLLBACK期间占用的行锁、间隙锁都不会释放。也就是说两个 XA 事务如果操作同一行数据后一个事务的等待时间不是本地 SQL 执行时间而是前一个全局事务从启动到结束的整个生命周期。我在实际项目里的体会是XA 非常适合事务短、分支少、行冲突小的业务。比如一个下单操作两个库各执行一条 SQL正常情况下从 PREPARE 到 COMMIT 也就是几十毫秒的事锁的代价可以接受。但如果你把外部 HTTP 调用、短信通知这类耗时的非数据库操作也塞进全局事务锁的持有时间会被拉长到秒级甚至分钟级那数据库基本就被锁死了。这也是面试里经常追问的点为什么 XA 强一致但是吞吐量上不去答案就在 PREPARE 到 COMMIT 之间的锁窗口。3. 手把手集成 Seata XA订单库库存库双写落地的完整过程3.1 前置准备Seata Server、数据库表、XA 支持检查先说明一下下面这套是基于 Spring Boot 2.7.x Seata 1.7.1 的常见组合也是目前社区里用得比较多的一套版本搭配。Seata 2.x 在配置上有些变化但整体思路一样。你需要的环境两个 MySQL 库推荐 8.0。本文示例是order_db和storage_db。Seata Server我用的是 1.7.1去 GitHub Releases 下载seata-server-1.7.1.tar.gz解压即可。一个 Spring Boot 工程准备集成多个数据源。启动 Seata Server 很简单默认的注册中心和配置中心都是 file 模式直接执行sh seata-server.sh -p 8091 -m file -h 127.0.0.1-p指定服务端口-m指定会话存储方式开发环境用 file 就够了生产环境建议用 db 模式否则 Seata Server 重启之后全局事务记录会丢。建表语句是后面要用到的-- order_db CREATE TABLE t_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL COMMENT 订单号, commodity_code varchar(32) NOT NULL COMMENT 商品编码, count int NOT NULL COMMENT 数量, amount decimal(10,2) NOT NULL COMMENT 金额, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- storage_db CREATE TABLE t_storage ( id bigint NOT NULL AUTO_INCREMENT, commodity_code varchar(32) NOT NULL COMMENT 商品编码, count int NOT NULL COMMENT 剩余库存, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;检查数据库是否支持 XA最简单的办法是直接执行XA START test; XA END test; XA ROLLBACK test;能正常执行不报错说明你的 MySQL 版本和驱动支持 XA。这一步建议在集成 Seata 之前先做可以排除掉数据库本身的兼容性问题。3.2 依赖与配置一个容易漏配置的地方Maven 依赖里需要引入 Seata 的 Spring Boot Starter。如果你用的是 Spring Cloud Alibaba可以引入spring-cloud-starter-alibaba-seata它会自动带进来 Seata 的相关依赖。如果不用 Spring Cloud直接引入seata-spring-boot-starter也行。我这里以直接引入 seata-spring-boot-starter 为例dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.7.1/version /dependency然后在application.yml里加入 Seata 的基础配置seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group registry: type: file config: type: file service: vgroup-mapping: my_test_tx_group: default grouplist: default: 127.0.0.1:8091这几个配置项的含义分别说一下application-id当前服务的应用名Seata 用它识别是哪个服务发起的全局事务。tx-service-group事务服务分组可以理解为给当前服务定义的一个逻辑分组名称。vgroup-mapping.my_test_tx_group: default把逻辑分组映射到 Seata Server 集群的某个集群名。grouplist.default: 127.0.0.1:8091指定集群下 Seata Server 的实际地址就是上面启动的 8091 端口。这里最常见的坑是只配了tx-service-group忘了配 vgroup 映射或者映射关系配错结果服务启动时报no available service错误连不上 TC。配置文件路径在不同版本里略有差异1.7.1 这套配置是最常见的。3.3 数据源代理与业务代码XA 模式的重点在 DataSource不在 SQLXA 模式集成中最关键的一步是数据源代理。普通的数据源交给 MyBatis 管理无法接入 Seata必须用DataSourceProxyXA包一层。先定义两个底层的物理数据源然后分别创建对应的 XA 代理 BeanConfiguration public class DataSourceXAConfig { Bean ConfigurationProperties(prefix spring.datasource.order) public DataSource orderDataSource() { return new DruidDataSource(); } Bean public DataSource orderDataSourceXA() { return new DataSourceProxyXA(orderDataSource()); } Bean ConfigurationProperties(prefix spring.datasource.storage) public DataSource storageDataSource() { return new DruidDataSource(); } Bean public DataSource storageDataSourceXA() { return new DataSourceProxyXA(storageDataSource()); } }对应的配置文件spring: datasource: order: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/order_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root storage: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/storage_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root注意这里我没有给任何一个数据源加Primary原因后面第 4 章会详细讲。多个数据源的情况下最重要的是让 SqlSessionFactory 明确绑定到代理后的 XA 数据源上否则你写的 Mapper 实际用的是原生数据源XA 全局事务就不会生效。业务代码方面XA 模式的侵入性非常小只需要在方法上加一个注解Service public class OrderBusinessService { GlobalTransactional(name create-order-and-deduct-storage, rollbackFor Exception.class) public Order createOrderAndDeductStorage(OrderDTO dto) { // 1. 订单库插入订单 Order order new Order(); order.setOrderNo(dto.getOrderNo()); order.setCommodityCode(dto.getCommodityCode()); order.setCount(dto.getCount()); order.setAmount(dto.getAmount()); orderMapper.insert(order); // 2. 库存库扣减库存 int rows storageMapper.reduceStorage(dto.getCommodityCode(), dto.getCount()); if (rows 0) { throw new RuntimeException(库存不足扣减失败); } return order; } }GlobalTransactional注解就是 TM 的入口它会让当前线程绑定一个 XID同时让所有 RM 知道当前存在全局事务。里面就是普通的 Mapper 调用没有任何分布式事务的代码。看到这里你会发现XA 模式和 AT 模式在业务代码里几乎长得一模一样区别全在数据源代理上。XA 用的是DataSourceProxyXAAT 用的是DataSourceProxy两者底层执行的事务协议完全不同。3.4 验证事务造一次失败看回滚日志集成完先不要急着上线花十分钟做一个验证在扣减库存之后主动抛一个异常看看两个库的数据是不是都回滚了。GlobalTransactional(name create-order-and-deduct-storage, rollbackFor Exception.class) public Order createOrderAndDeductStorage(OrderDTO dto) { orderMapper.insert(order); storageMapper.reduceStorage(dto.getCommodityCode(), dto.getCount()); throw new RuntimeException(模拟异常触发回滚); }正常情况下的结果是order_db里没有新订单storage_db里库存也没有扣。从日志层面看Seata 会打印分支注册、全局事务回滚的回执信息大致能看到Begin new global transaction: xid127.0.0.1:8091:123456789, namecreate-order-and-deduct-storage Registering branch: xid127.0.0.1:8091:123456789, branchId1 Handling new global transaction: xid127.0.0.1:8091:123456789, statusRollbacking Branch transaction report: xid127.0.0.1:8091:123456789, branchId1, statusRollbacked看到statusRollbacked且两个库都没有数据说明 XA 模式已经生效。这时候再去数据库执行XA RECOVER;应该看不到任何残留的 XA 事务说明连接没有被悬挂。我自己在测试时习惯把logging.level.io.seatadebug打开确认每个分支的执行状态。XA 模式一旦有问题往往不是代码逻辑问题而是某个环境细节没配上调试日志能帮你快速定位是哪一步断了。4. XA 模式踩坑实录悬挂事务、连接耗尽与 XID 丢失的排查链路4.1 数据库悬挂事务应用挂了一次库表被锁死这是一个真实的生产事故。某天晚上 DBA 发来告警说order_db的t_order表 UPDATE 全部卡住CPU 不高但所有写操作都过不去。第一反应不是去看应用日志而是先看数据库侧。SHOW FULL PROCESSLIST一看有一大堆连接堆积在同一个事务上状态是Updating或者Sleep它们都在等一把锁。再执行XA RECOVER;果然发现有一条处于 ACTIVE 状态的 XA 事务格式类似XA RECOVER; ------------------------------------------------------ | formatID | gtrid_length | bqual_length | data | ------------------------------------------------------ | 1 | 88 | 0 | 127.0.0.1:8091:xxx | ------------------------------------------------------这条记录的 data 字段就是 Seata 的 XID。为什么会有悬挂事务原因是应用在执行完XA PREPARE之后、还没收到全局 COMMIT 指令之前JVM 发生了 OOM直接被 kill 掉了。进程没了物理连接被操作系统回收但数据库侧的事务并没有收到XA COMMIT或XA ROLLBACK于是这条 XA 事务就永远停留在 prepare 状态手里的锁也不会释放。处理方法分两步。紧急处理根据XA RECOVER输出的 XID手动执行XA ROLLBACK 127.0.0.1:8091:xxx;或者如果业务上确认可以提交就执行XA COMMIT。我通常会先确认这个事务对应的业务是否已经不可追踪然后直接回滚锁立刻释放。长期处理是我后来在架构上做的几件事:第一全局事务的方法里绝不放大段耗时逻辑尽量保证 PREPARE 到 COMMIT 的窗口在几十毫秒以内第二给 Seata Server 配置合理的全局事务超时GlobalTransactional注解里的timeoutMills属性可以设置默认 60000 毫秒对我这个场景太长了我调到了 10000第三DBA 侧做了巡检脚本定期执行XA RECOVER发现悬挂事务立即告警。悬挂事务是 XA 模式最需要防范的故障因为它一发生就是整个表卡死影响面非常大。4.2 连接池被榨干XA“占用连接”的代价第二个坑发生在压测阶段。并发一上来应用开始大量报获取连接超时日志里出现Cannot get a connection, pool error Timeout waiting for connection当时我第一反应是连接池配置太小把最大连接数调大了还是有这个问题而且调大之后数据库连接数也紧张起来。后来看 Druid 的监控面板才发现活跃连接数长期处于高位而且很多连接是同一个线程持有的好几秒都不释放。想明白原理之后这个问题就清晰了。XA 事务在二阶段结束之前每个参与分支的 RM 都会占用一条物理数据库连接。也就是说一个全局事务如果涉及两个库它至少要持有两条物理连接时间覆盖整个全局事务的生命周期。如果你的连接池最大连接数是 20又有 15 个并发请求同时在执行全局事务连接池可能直接被打满后面新的请求连连接都拿不到。而且这里还有一个隐藏的恶性循环连接池被打满之后Seata Server 给 RM 发送回滚指令RM 要执行XA ROLLBACK也需要从连接池获取连接如果连接池里的连接全被全局事务占用了回滚动作也执行不了变成“想回滚都没资源回滚”的局面。我的调整方案是把每个数据源的最大连接数从 20 提到 50最小空闲连接数从 5 提到 10。给GlobalTransactional方法瘦身把不涉及数据库的操作全部移到事务方法外面。给 Druid 配置合理的maxWait和validationQuery确保连接池等待有上限不会无限阻塞。生产环境连接池大小要结合“全局事务并发数”和“每个事务占用的连接数”一起估算而不是只按普通查询的并发量算。这里也要提醒一句不要为了缓解连接池压力给 Druid 开启removeAbandoned。这个配置会把“看起来很久没动的连接”强制回收但在 XA 模式下一个连接可能在 PREPARE 之后等待全局决策这个等待是正常的强制回收物理连接会直接导致数据库侧的 XA 事务悬挂比连接池耗尽更可怕。4.3 XID 没传过去自研 RPC 环境下全局事务悄悄失效第三个坑比较隐蔽。现象是两个服务都接入了 SeataA 服务调用 B 服务完成下单A 服务里加了GlobalTransactional结果测试时发现 A 服务的订单库回滚了但 B 服务的库存库没回滚。排查这种问题第一件事是看 B 服务的日志确认它有没有识别到 XID。Seata 有一个专门的无侵入设计在线程上下文里维护一个RootContext.XIDRM 在执行 SQL 的时候会检查这个 XID 是否存在存在才走全局事务分支。XID 在服务之间传递靠的是 RPC 框架的隐式传参。Spring Cloud 生态里Seata 会通过请求头的TX_XID字段自动透传如果是基于 Feign/RestTemplate 的调用通常不用你操心。但我们的生产环境里有一部分服务用的是自研的 RPC 框架不走 Spring Cloud 的标准链路XID 就没法自动传过去。B 服务收到请求的时候线程上下文里没有 XID于是它把库存库的扣减当成普通本地事务直接提交了全局事务根本管不到它。排查链路很简单在 A 服务发起 RPC 调用的地方打印RootContext.getXID()在 B 服务入口打印RootContext.getXID()。如果前者有值后者为 null基本可以确认是透传问题。修复方式是写一个通用的过滤器或者拦截器在服务出口把当前 XID 塞到请求头在服务入口取出来绑定到根上下文。伪代码大概是这样的// 出口过滤器RPC 发送前执行 String xid RootContext.getXID(); rpcRequest.setHeader(TX_XID, xid); // 入口拦截器RPC 接收后执行 String xid rpcRequest.getHeader(TX_XID); if (StringUtils.hasText(xid)) { RootContext.bind(xid); }另外还有一种情况即便在一个服务内部如果你在GlobalTransactional方法里手动开了线程池去执行某个数据库操作子线程里也没有 XID需要把 XID 手动传到子线程再绑定。这个不只在 XA 模式所有 Seata 模式都一样但 XA 模式因为对强一致要求高XID 丢失的后果被放大了所以排查的时候要格外谨慎。4.4 数据库版本与驱动不兼容MySQL 老版本的 XA 大坑第四个坑属于选型问题但踩的人非常多。MySQL 对 XA 的支持并不是所有版本都一样完善。MySQL 5.6 及之前版本对 XA 的支持不够稳定不建议在生产环境直接跑 Seata XA。MySQL 5.7 支持 XA但如果并发事务多历史上出现过一些 prepare 阶段一致性问题而且还存在多个 XA 事务并发时的性能瓶颈。MySQL 8.0 对 XA 的支持相对成熟是我目前在 XA 模式下的首选。另外驱动也有讲究。低版本的 MySQL JDBC 驱动可能不支持 XA 连接至少要使用mysql-connector-java5.1.40 以上版本但连接串里很可能需要额外参数。我的经验是直接用 MySQL 8.0 对应的com.mysql.cj.jdbc.Driver配合mysql-connector-j8.0.x 驱动XA 行为最稳定。Oracle 和 PostgreSQL 的 XA 支持比 MySQL 更完善Oracle 要用OracleXADataSource来创建 XA 连接而不是普通的DriverManagerDataSource。SQL Server 的 XA 支持依赖 Windows 的 MSDTC 服务配置复杂度更高如果一块评估这三种数据库Oracle 接 Seata XA 的阻力最小MySQL 8.0 次之SQL Server 最折腾。验证数据库到底支持不支持 XA别光看文档直接到目标环境执行一遍XA START、XA END、XA ROLLBACK这是最靠谱的。我见过有项目用的还是 MySQL 5.5集成的时候直接报语法错误跟 Seata 本身完全无关纯属数据库版本不支持。5. 架构决策参考XA 和 AT 怎么选以及哪些场景别用 XA5.1 XA vs AT 的对照表与选择逻辑很多人会在 AT 和 XA 之间纠结我用一张表把它们的核心差异列清楚对比项XA 模式AT 模式一致性数据库层面强一致Seata 层最终一致业务侵入性数据源代理SQL 无侵入数据源代理 需要 undo_log 表锁持有时间prepare 到 commit 之间锁不释放本地事务提交即释放但有全局锁回滚方式数据库原生 XA ROLLBACK通过 undo_log 逆操作对 SQL 的依赖不关心 SQL 内容只要事务型数据库需要 SQL 可解析才能生成补偿 SQL并发性能偏低锁窗口长偏高本地提交早对数据库要求必须支持 XA 协议主流关系型数据库基本都可以对 DBA 友好度高原生协议不需要额外表低要监控 undo_log 和全局锁推荐场景短事务、强一致、低冲突长事务、高并发、最终一致可接受选型的核心逻辑就三条。第一业务能不能接受最终一致不能接受且链路短选 XA。第二并发冲突是不是很大比如两个全局事务经常操作同一行数据XA 的锁等待会拖垮整个系统这种场景宁可放弃强一致也不要硬上 XA。第三数据库是不是你能掌控的如果 DBA 对 XA 不熟悉或者数据库版本太老AT 可能是更务实的方案。这里补充一个面试里经常问到的点为什么 AT 模式比 XA 模式性能好因为 AT 模式一阶段直接提交本地事务锁在本地事务提交时就释放了虽然它用全局锁表保护了中间状态但全局锁的粒度比数据库行锁细灵活性更高。XA 模式必须把锁一直保持到全局事务结束所以并发能力天然弱一个档次。5.2 三个别用 XA 的真实场景第一个场景全局事务方法里有外部系统调用。比如下单流程里要调内部的会员服务、短信服务这些外部调用的耗时完全不可控一旦网络抖动几十秒数据库锁就被拉长几十秒。正确做法是把外部调用移出GlobalTransactional方法或者考虑用 SAGA 模式逐个分支提交本地事务失败时通过反向操作补偿。第二个场景两个库之间的行冲突概率很高。比如一个秒杀场景所有用户都去抢同一个商品的库存扣库存的行锁竞争极其激烈。XA 模式下所有全局事务都必须等前一个事务的库存扣减完全 commit 之后才能继续这个等待时间被直接放大到全局事务的完整生命周期结果就是系统吞吐量断崖式下跌。这种场景我建议放弃强一致用 Redis 之类的组件做库存扣减再异步同步到数据库用最终一致来换取并发能力。第三个场景数据库版本太老。如果你还在用 MySQL 5.6 或者 5.7 的早期版本老老实实升到 8.0 再考虑 XA。版本问题不像代码问题那样可以绕过去它是数据库层面的能力缺失硬上就是给自己埋雷。5.3 关于 Seata 整体落地的几条长期维护经验最后分享几条我维护这套系统几年下来的实际经验不一定都是 XA 独有的但对 Seata 在线上稳定运行很重要。第一个经验监控一定要做全。Seata Server 的会话数量、分支数量、全局事务状态这些指标对判断系统健康度非常关键。数据库侧要重点看Com_xa_start、Com_xa_prepare、Com_xa_commit、Com_xa_rollback这些计数器的增量如果Com_xa_rollback突然变多说明业务上开始出现大量回滚需要及时关注。第二个经验日志和 XID 要贯穿全链路。每个全局事务的 XID 必须打到业务日志里排查问题的时候你要能从一个异常堆栈一路追到 TC 的事务记录确认这个分支最终是提交还是回滚。很多分布式事务问题难排查不是因为技术复杂而是因为日志里没有 XID根本找不到线索。第三个经验别试图用一个模式解决所有分布式事务问题。我见过有的团队接入 Seata 之后所有跨库操作统一用 XA结果复杂业务链路里出现各种超时也见过团队因为 AT 模式性能好连转账这种强一致场景也用 AT结果出现极端情况下的补偿 SQL 拼接问题。更合理的做法是按业务场景逐个评估短链路强一致用 XA长链路最终一致用 SAGA 或消息队列多服务编排用 TCC把模式的边界划分清楚而不是一刀切。这些坑没有一个是从官方文档里可以直接查到的都是在生产环境的凌晨和压测报告里一个个逼出来的。分布式事务本身就没有银弹XA 模式能给你的是数据库层面的强一致承诺但代价是锁、连接和超时这些资源的全盘权衡。希望你读完这篇之后能对自己的业务场景做出清晰判断少踩几个我踩过的坑。
返回列表