ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Seata XA模式实战:基于两阶段提交的强一致性分布式事务方案

Seata XA模式实战:基于两阶段提交的强一致性分布式事务方案 1. 为什么我在订单系统里放弃了AT模式改用XA做分布式事务最怕的就是事务做到一半数据不知道对不对。我这两年一直在维护一套订单与库存系统最开始用的是Seata的AT模式后来压测一上量就出各种对账不平的问题排查下来发现AT模式的undo_log在复杂业务链路里很容易出脏读和回滚丢失。后来我换成了Seata的XA模式一条路走到黑反而省心很多。先说结论XA模式的核心价值在于它直接依赖数据库原生的两阶段提交协议数据一致性由数据库来兜底而不是靠业务代码或者中间件去模拟事务。对一致性要求特别高、并发冲突相对可控的场景比如订单扣库存、账户余额变更XA模式是我个人认为最稳的选型。这篇文章我会从Seata的整体架构开始讲一步步拆解XA模式下TM、TC、RM这三大角色是怎么协同工作的还会给出完整的Spring Boot接入配置、MySQL环境下的实操步骤以及我实际踩过的坑。无论你是刚开始学分布式事务还是已经在项目里做了方案选型这篇文章应该都能帮到你。2. Seata是什么以及它解决什么问题2.1 从一次扣款成功但库存扣减失败说起先还原一个最常见的业务场景用户在电商平台下单系统要做两件事——扣减账户余额扣减商品库存。如果是单库单应用一个本地事务就能搞定。但在微服务架构下账户服务和库存服务各自独立部署、独立数据库本地事务的ACID就失效了。最简单的方案是先扣钱再扣库存然后祈祷第二步一定成功。但现实是服务会宕机、接口会超时、数据库会死锁第二步一旦失败钱扣了货没发用户投诉就来了。反过来先扣库存再扣钱又会造成库存被占住但订单没生成。这就是分布式事务要解决的经典问题。SeataSimple Extensible Autonomous Transaction Architecture就是专门干这个事的开源中间件。它把分布式事务拆成了三个角色TMTransaction Manager事务管理器、TCTransaction Coordinator事务协调器、RMResource Manager资源管理器。我后面会详细拆这三个角色这里先建立整体认知。2.2 Seata四种模式的选型逻辑Seata目前主流的模式有四种AT、TCC、Saga、XA。很多人一上来就默认用AT模式因为它在代码层面几乎无侵入但在高一致性场景下AT模式的软事务特性会埋雷。AT模式的核心思路是补偿本地事务正常执行Seata自动生成undo_log如果后续分支事务失败通过反向SQL回滚。这种模式的优点是好接入但代价是全局读未提交、隔离级别低而且依赖全局锁控制并发性能在大流量下会明显下滑。TCC模式需要业务方手动实现Try、Confirm、Cancel三个方法灵活性高控制力强但开发成本高业务代码侵入严重适合那种可以明确拆分预操作和确认操作的场景。Saga模式适合长事务强调的是业务流程的编排和补偿但它没有隔离性如果中间步骤对其他事务可见容易出现脏读不适合资金类、库存类的强一致场景。XA模式走的是数据库原生的两阶段提交RM直接通过数据库的XA协议参与全局事务。它的特点是数据一致性由数据库保证隔离性最好代码侵入低但性能天花板受限于数据库本身且要求数据库支持XA。我最终选择XA模式核心原因有两条第一我们的订单库存场景对一致性要求是硬性的不能出现钱扣了库存没扣或者库存扣了钱没扣第二我们的并发量还没到需要靠分布式事务中间件做性能优化的程度数据库本身的XA能力足够扛住。3. XA模式的核心机制与角色拆解3.1 数据库原生的XA协议是怎么工作的要理解Seata的XA模式必须先理解数据库自带的XA协议。XA协议是X/Open组织定义的两阶段提交规范它把事务拆成两个阶段Prepare阶段和Commit/Rollback阶段。Prepare阶段事务协调者在Seata里是TC向所有参与事务的数据库RM发送prepare请求每个数据库执行SQL、写redo/undo日志但不提交事务只返回我准备好了或我失败了。这个阶段不会释放行锁所以其他事务读不到未提交的数据。Commit阶段如果所有RM都返回prepare成功TC就向所有RM发送commit指令各数据库正式提交事务。如果任何一个RM返回prepare失败TC就向所有RM发送rollback指令所有数据库回滚本地事务。这里有一个关键点两阶段提交的prepare阶段实际上已经把数据变更持久化到了数据库内部只是对业务不可见。所以一旦prepare全部成功commit阶段几乎不可能失败这就是XA模式数据一致性极强的原因。3.2 TM、TC、RM在XA模式下的具体行为在Seata框架里这三个角色的协作流程是这样的TM事务管理器是全局事务的发起方通常就是业务入口服务。比如订单服务下单时TM会向TC发起全局事务注册拿到一个全局事务IDXID这个XID会通过微服务调用链传递到下游服务。TC事务协调器是独立的Seata服务端负责全局事务的注册、分支事务的注册、状态维护以及最终的二阶段提交/回滚指令的下发。它是分布式事务的大脑。RM资源管理器在XA模式下就是数据库本身。Seata客户端通过DataSourceProxy包装数据源把业务SQL交给MySQL执行同时MySQL以XA协议的方式向TC报告分支事务的prepare结果。具体执行链路是这样的TM开启全局事务拿到XID后订单服务和库存服务各自动执行本地SQL。和AT模式不同XA模式下本地SQL执行时并不会立即提交而是进入prepare状态事务资源行锁一直被持有。TC收集到所有RM的prepare结果后统一决策全部成功就下发commit任一失败就下发rollback。3.3 XA模式和AT模式的本质区别很多人分不清XA和AT这里我直接用一张表说明白对比项AT模式XA模式事务一致性保证最终一致有中间状态强一致无中间状态回滚机制反向SQL补偿undo_log数据库原生事务回滚隔离级别全局读未提交有脏读风险数据库本地隔离级别可配置代码侵入极低几乎透明极低需配置数据源代理性能影响依赖全局锁冲突时性能下降依赖数据库XA实现prepare阶段持锁适用场景高并发、性能敏感、弱一致性可容忍强一致、金融交易、订单库存我举个例子说明两者在数据一致性上的差异。假设库存服务在事务内执行了UPDATE stock SET count count - 1 WHERE id 1AT模式下这个变更对全局事务外部是可见的全局读未提交如果此时另一个线程读到count99并基于这个值做判断会产生脏读。XA模式下这个变更停留在数据库内部的prepare状态其他事务完全不可见除非全局事务提交否则数据不会对外暴露。这里面有一个很重要的结论如果你的业务对数据一致性要求是严格意义上的要么全成功要么全失败且过程中不能被读到中间态XA模式是更安全的选择。AT模式的最终一致在实际业务里往往靠对账任务兜底增加了一层运维复杂度。4. 完整实操Spring Boot MySQL接入Seata XA模式4.1 部署Seata ServerTCSeata Server的部署方式比较灵活可以用Docker、二进制包或者Kubernetes。我这里以最常用的Docker部署为例。拉取镜像并启动docker run --name seata-server -d \ -p 8091:8091 \ -p 7091:7091 \ -e SEATA_IP192.168.1.100 \ -e SEATA_PORT8091 \ -e STORE_MODEdb \ seataio/seata-server:1.8.0需要注意几个参数。SEATA_IP要填服务器实际IP不能填localhost或容器IP否则客户端连不上。STORE_MODEdb表示事务日志存到数据库里生产环境千万不要用file模式否则Seata Server重启后事务状态会丢失已注册的全局事务可能永久悬挂。Seata Server还需要初始化数据库表官方SQL脚本在seata-server的script目录下包括global_table、branch_table、lock_table三张表。我踩过的坑是MySQL的版本如果高于8.0官方脚本里的varchar长度要调整否则启动时会报Data too long。4.2 业务服务改造引入依赖与配置数据源Spring Boot项目的pom.xml里加依赖dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-seata/artifactId version2021.0.5.0/version /dependency然后是application.yml配置spring: datasource: url: jdbc:mysql://localhost:3306/order_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver cloud: alibaba: seata: tx-service-group: my_test_tx_group seata: application-id: order-service tx-service-group: my_test_tx_group registry: type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP service: vgroup-mapping: my_test_tx_group: default >Configuration public class SeataConfig { Bean ConfigurationProperties(prefix spring.datasource) public DataSource dataSource() { return new DruidDataSource(); } Bean public DataSource seataDataSource(DataSource dataSource) { return new DataSourceProxyXA(dataSource); } }DataSourceProxyXA是XA模式专用的代理类它在内部通过XAConnection管理数据库的XA事务分支。这里要注意如果项目同时用了MyBatisSqlSessionFactory的dataSource属性要指向seataDataSource否则事务不会进入Seata的管控范围。4.3 核心业务代码全局事务注解与事务传播业务代码层面的改动其实很小核心就是GlobalTransactional注解Service public class OrderServiceImpl implements OrderService { GlobalTransactional(name create-order, rollbackFor Exception.class) public void createOrder(OrderDTO orderDTO) { // 扣减账户余额 accountService.deductBalance(orderDTO.getUserId(), orderDTO.getAmount()); // 扣减库存 inventoryService.deductStock(orderDTO.getSkuId(), orderDTO.getQuantity()); // 创建订单 saveOrder(orderDTO); } }这是TM端也就是全局事务的入口。注意GlobalTransactional必须加在调用链的最外层方法上而不是每个子方法都加。如果多个方法都加了注解会发生嵌套全局事务Seata虽然会处理但逻辑会变得复杂而且嵌套事务的回滚语义很容易搞混。下游的库存服务和账户服务不需要加GlobalTransactional只需要用普通的Transactional跑本地事务Seata会自动把本地事务纳入全局事务的XA分支。这里我想特别强调一点下游服务在XA模式下不能手动设置事务传播行为为REQUIRES_NEW因为XA事务要求同一个全局事务下的所有分支必须处于同一个数据库连接事务边界内。如果你在库存服务里用了REQUIRES_NEWMySQL会报XAER_RMFAIL类似的错误因为新连接不在全局事务上下文中。4.4 关键参数调优事务超时、重试与连接池XA模式有两个参数特别值得关注超时时间和连接池大小。在Seata Server端server.transaction.maxTimeout控制全局事务的最大超时时间默认是60秒。如果业务链路比较长比如下单后要调第三方接口建议调大到120秒或更大否则全局事务会提前超时被TC回滚而业务线程还在跑本地事务造成资源悬挂。连接池方面XA事务在prepare阶段会一直持有数据库连接这意味着连接池的最大活跃连接数直接决定了同一时刻能并发处理多少个全局事务分支。我经历了一次压测事故默认连接池20个连接压到200并发时数据库连接直接耗尽大量SQL等待获取连接接口超时率飙到70%。最后把Druid连接池调到100并且限制了全局事务的并发入口情况才算稳定。spring: datasource: druid: initial-size: 20 min-idle: 20 max-active: 100 max-wait: 60000还有一个容易忽略的MySQL参数innodb_lock_wait_timeout。XA模式下prepare阶段的行锁一直持有到全局commit所以锁等待时间如果设置太短高并发下很容易出现大量锁等待超时。我建议把innodb_lock_wait_timeout调整到50秒左右并结合业务并发量做压测验证。5. 我踩过的那些坑XA模式实战问题排查5.1 报错 XAER_RMFAILMySQL版本与XA协议的兼容性这个问题我印象太深了。第一次把Seata切换到XA模式压测没跑多久日志里就刷出来XAER_RMFAIL。查了一圈发现问题出在MySQL驱动的版本上。旧的mysql-connector-java 5.1.x对XA协议的实现和MySQL 8.0服务端的XA事务管理有兼容性问题导致prepare阶段返回失败。解决办法简单粗暴升级驱动。dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency升级后XAER_RMFAIL没再出现过。这里也提醒一下如果你的项目还在用MySQL 5.7驱动版本用5.1.49以上也可以但最好是8.x系列兼容性和性能都更好。5.2 全局事务不生效数据源代理没生效有个同事接XA模式后反馈说自己明明加了GlobalTransactional但库存扣减成功后手动抛异常账户余额居然没回滚。排查代码发现他的MyBatis SqlSessionFactoryBuilder直接使用了原始数据源绕过了Seata代理事务分支根本没有注册进去。这种问题在Spring Boot MyBatis组合下非常常见。要点是MyBatis使用的DataSource必须指向Seata代理后的DataSource对象不能是原始DruidDataSource。已在上文4.2里给出了配置方式这里再次强调一遍——如果你用Bean方式手动配置了DataSource务必确认SqlSessionFactory的dataSource引用了代理对象。一个快速验证方法在Seata Server的日志里如果能看到分支事务注册记录branch register说明代理生效了。如果全局事务执行完日志里安安静静那大概率是代理配置有问题。5.3 MySQL 8.0.22版本遇到的XA事务悬空问题这是个非常隐蔽的坑。MySQL官方在8.0.22版本修复了一个XA事务相关的bug但也引入了一个新问题XA事务在prepare阶段执行了DDL语句比如建表、改字段会导致事务悬空无法commit也无法rollback。我们当时在一个全局事务里调用了一个工具类它内部执行了CREATE TEMPORARY TABLE结果事务挂死锁表锁了三个小时。排查后确认是数据库版本问题最后把MySQL小版本升级到8.0.29以后解决了。由此得出的经验是XA模式下不要在全局事务内执行DDL或临时表相关操作即使语法上允许也必须警惕数据库版本的兼容性风险。5.4 二阶段提交失败时的人工补偿方案尽管XA模式一致性很强但强不是绝对的。TC下发commit指令后如果某个数据库节点恰好宕机commit无法送达数据状态就停留在prepare状态。MySQL重启后在recovery阶段会重新读取XA事务列表此时需要人工介入决定事务走向。我的做法是搭建一个监控任务定期扫描information_schema.XA_TRANSACTIONS表把滞留时间超过阈值的事务捞出来告警。如果确认所有业务操作都执行成功了就手动执行XA COMMIT如果有疑点就XA ROLLBACK后再人工对账。这个经验可能有些偏门但真的遇到宕机场景时能救急。5.5 主从延迟对XA模式的影响如果数据库架构是主从复制XA模式下要特别注意主从延迟问题。全局事务在master上commit后如果读操作路由到了slave而slave还没同步到这条变更业务上就会看到事务结束了但数据没变的错觉。这不影响一致性但对用户体验不友好。解决办法是设置读操作的事务隔离级别为REQUIRES_NEW并把事务路由强制指向master。如果你用的是ShardingSphere或MyCat这类中间件可以在SQL注释里加/* routing:master */强制走主库。6. 为什么说XA模式在订单库存场景是硬选择6.1 从CAP理论看XA模式的定位很多资料说分布式事务违反了CAP的可用性实际上需要准确一点XA模式牺牲的是极端情况下的可用性换来的是强一致性。在下单扣库存这个场景里业务上可以接受事务执行时系统短暂不可用但绝对不能接受账不平。这就是为什么金融、电商核心交易链路普遍倾向XA或者TCC这样的强一致方案。CAP理论里XA模式走的是CP路线。它不是性能最优而是正确性最优。当你的团队没有专职DBA、没有复杂对账系统、又不想花大力气写TCC三个方法时XA模式是投入产出比非常高的选项。6.2 什么情况下千万不要用XA模式XA模式不是万能的我说几个劝退场景。第一个是高并发秒杀。秒杀场景下库存扣减的并发冲突极高XA模式的prepare阶段会长时间持有行锁后面的请求全部排队等待TPS会非常难看。这种场景更合适用Redis预扣减库存 异步对账的方案或者AT模式配合全局锁优化。第二个是跨异构资源的事务。比如你要同时操作MySQL和Redis或者MySQL和ElasticsearchXA协议不支持这些非关系型存储参与两阶段提交。这种情况只能转向Saga或者TCC。第三个是长事务。XA要求所有分支事务在prepare后持续持有资源如果全局事务执行时间过长比如跨分钟级数据库锁等待和连接占用都会成为瓶颈。我个人的经验值是全局事务执行时间超过5秒的场景XA模式就要慎用了。6.3 XA模式在项目里的后续优化方向在XA模式稳定运行一段时间后我做了两个优化动作。第一个是引入了Seata的allowCrossBranchCommit参数让分支事务在prepare完成后立即提交本地事务减少锁持有时间。这个参数对性能提升明显但牺牲了全局原子性适合那些允许短暂不一致、最终一致的旁路数据不适合核心账务。第二个是改造了业务表的索引设计。XA模式下行锁是业务行的排他锁如果UPDATE语句的WHERE条件没有走索引会导致间隙锁甚至表锁性能急剧恶化。我给库存表、账户表的核心更新条件都加了联合索引实测锁等待时间下降了60%以上。7. 写在最后XA模式的适用范围与我的真实感受从AT切到XA的这半年我最大的感受是X A模式不是一个高深莫测的技术它其实就是把数据库本来就有的能力用起来了。你不需要理解什么复杂的补偿逻辑只要保证数据库支持XA、Seata配置正确事务一致性就有数据库这个老大哥在背后兜底。对团队来说心智负担是最低的。当然XA模式也有明显的短板性能和并发吞吐不如专门为高并发设计的最终一致方案。但在订单、库存、账户这类强一致场景我依然愿意把它排在首选。技术选型没有银弹关键是你得清楚自己的业务到底需要什么。如果你的系统也处在并发量中等、一致性要求很高的阶段可以放心地把XA模式纳入考量按照我上面分享的步骤跑一遍大概率能省掉不少折腾的时间。最后再分享一个小技巧无论你最终选择哪种分布式事务方案一定要把全局事务的关键日志打印出来至少包含XID、分支ID、事务状态变化。排查线上问题时这些日志是你唯一的线索。我在项目里用了Seata自带的GlobalTransactionContext打印XID到MDC配合日志平台的TraceId检索定位问题速度快了很多。
返回列表