分库分表做完之后,紧接着找上门的就是分布式事务。最近好几个朋友私下问我:Sharding-JDBC这边已经按订单ID把数据拆到了多个库里,但一个下单操作要同时写订单库和明细库,万一明细库超时,订单库已经写进去的那条记录要怎么处理?这就是典型的分布式事务问题。这篇借着整合MyBatis-Plus的过程,把基于XA协议的分布式事务从原理到落地完整捋一遍,顺便把那些文档里查不到、只能靠踩坑换来的细节一并交代清楚。
这篇适合谁看?如果你正在用ShardingSphere-JDBC(也就是老一批资料里写的Sharding-JDBC)做分库分表,接下来必须解决跨库事务,并且希望优先上强一致的方案,那么本文可以给你一条完整的落地路径。内容会从XA的两阶段提交原理讲起,然后给出一套可直接抄的Spring Boot + MyBatis-Plus + ShardingSphere-JDBC的配置和代码,最后集中梳理五个实测中容易翻车的点。我不打算把系列前几篇的分片规则再复述一遍,但会把与本篇相关的数据源托管、逻辑表配置重新交代清楚,保证单独拿这篇也能照着操作。
1. 分库分表之后,为什么你原来的事务一夜之间失效
1.1 从一次“跨库下单失败”说起
先说一个很典型的场景:订单表按用户ID取模拆到四个库,每个库里有订单主表、订单明细表、库存表。下单接口的逻辑很简单——插入订单主表、插入明细表、扣减库存。分库之前,这三个操作都在同一个数据库里,Spring 的@Transactional包住就算完事;分库之后,订单主表和明细表可能在ds0,库存表在ds1,三个操作被路由到了两个不同库的连接上。
这时候问题就来了:本地事务的提交和回滚是跟着单个连接走的,而你的三个操作分散在两个连接上,Spring 的@Transactional依然能开启事务,但只有它内部管理的那个主连接上的操作会被提交或回滚。另一个库的操作如果已经提交了,那这个库回滚了也没用——数据就处于“一半成功、一半失败”的状态。这是分布式事务的第一性原理问题:本地事务的边界就是单库连接边界,连接跨出去,事务就管不到了。
我见过不少团队在这个阶段的第一反应是“把三个操作放到一个 Service 方法里,用@Transactional包起来”,试完之后发现回滚时只回滚了一个库,于是怀疑是 MyBatis-Plus 版本问题或者缓存问题,排查方向完全跑偏。实际上不是代码写错,而是事务模型本身没有升级。
1.2 本地事务、分布式事务、柔性事务,到底在选什么
接触分布式事务方案时,你会看到一堆名词:两阶段提交、TCC、本地消息表、Saga、事务消息。如果只是照着选一个用,很容易被“分布式事务”四个字统称为同一个东西,但它们的出发点和代价差异非常大。
简化的分类方法是看一致性目标:
- XA/两阶段提交:追求“全局强一致”,所有参与库要么全部提交,要么全部回滚。代价是同步阻塞,性能相对差,但事务语义和单库事务最接近。
- TCC:把每个操作拆成Try、Confirm、Cancel三个步骤,用业务补偿的方式实现最终一致。性能和灵活性更好,但每一个参与方都要实现三套接口,改造量大。
- 本地消息表/事务消息:基于消息中间件实现最终一致,允许中间状态存在,适合链路长、容忍异步的场景。
ShardingSphere 在这条路线里的定位很明确:它提供的是基于XA协议的分布式事务,属于强一致方案。本项目标题中“解决分布式事务(基于XA)”的实际意思是:在分片数据源之上,由 ShardingSphere 接管多库连接,用XA协议让所有分片库在同一个全局事务里协调提交或回滚。下面就把最关键的两阶段提交原理拆开讲。
1.3 两个常见误解先排掉
误解一:只要加了@ShardingSphereTransactionType(TransactionType.XA),XA事务就不需要Scaling,性能损失可以忽略。实际恰恰相反,XA事务每个分片库的连接要一直持有到全局提交,期间连接不能被释放回连接池,所以并发一高,性能下降明显。后面会单独讲怎么补偿这个代价。
误解二:XA事务和MyBatis-Plus的@Transactional是互斥的。排除了前一种误解之后,有人又说“用了ShardingSphere就不能用Spring事务注解了”。这个是典型的理解偏差:@Transactional仍然是整个事务的外层入口,只不过事务内部连接的管理权交给ShardingSphere,XA实现才真正生效。两者是“嵌套配合”而不是“二选一”。
2. XA两阶段提交:准备、提交、回滚的底层逻辑
2.1 Prepare阶段:资源管理器先记账
XA 协议定义了三个角色:事务管理器(Transaction Manager,简称TM)、资源管理器(Resource Manager,简称RM)、应用程序。在ShardingSphere的XA实现里,TM对应XA事务管理器(默认是Atomikos),每个分片库连接对应的就是RM。整个全局事务分两阶段走。
第一阶段叫Prepare(准备)阶段。全局事务开始后,应用正常执行SQL,各分片库连接上执行的操作不会立即生效并释放,而是记录在各自库内部的事务日志里。流程走到准备提交时,TM会向每个RM发Prepare指令。RM收到指令后,把所有已执行SQL涉及的资源锁住,把要提交的数据状态固化到本地事务记录里,然后向TM汇报:我这边准备好了,可以提交;或者:我这边出错了,无法提交。
用生活化的类比,这一阶段相当于所有参与方先把账目底稿拿出来核对完,签上“确认无误”的字,但还没有真正把最终账单锁进保险柜。这一阶段的核心价值是:试图提前暴露无法提交的分支,避免到提交阶段才发现某个库写不进去。
这里有一个容易忽略的关键点:Prepare阶段不是只做一次全局快照,而是会持有连接和行级锁。也就是说,从Prepare开始,一直到全局事务结束,参与库的相关数据行都是被锁住的。如果你在分片场景里有跨库查询同一行的操作,就可能在这里出现锁等待。
2.2 Commit/Rollback阶段:全局协调者的最终拍板
第二阶段叫Commit(提交)阶段。TM在收到所有RM的“已准备”回复后,向各RM发送Commit指令。RM执行真正的提交,释放锁,返回成功。如果有任何一个RM在Prepare阶段报错,或者某个RM迟迟没有响应,TM就会向所有RM发送Rollback指令,各RM回滚本地事务,释放锁,全局事务结束。
这就是“要么都提交,要么都回滚”的实现方式。第二阶段的潜在风险在“某个RM已经提交成功,但TM在发送Commit指令前崩溃”,此时全局结果不确定,需要依赖事务管理器日志和连接重放机制来修复,XA协议里的Heuristic Exception就是描述这类结果不确定的异常。
ShardingSphere把这一整套交给了Atomikos默认实现。开发者的主要工作不是自己实现协议,而是导入依赖、打开XA开关、保证连接可被RX(XA连接)使用。下面看ShardingSphere境地怎么选这三种实现。
2.3 三种XA管理器横向对比:Atomikos赢在哪
ShardingSphere通过SPI方式集成了三个可用的事务管理器。如果不做任何配置,默认使用Atomikos。我的建议也是小团队、中小项目直接用默认的Atomikos,不做特殊定制。
| 事务管理器 | 维护活跃度 | 配置复杂度 | 日志表 | 适用场景 |
|---|---|---|---|---|
| Atomikos | 社区活跃,资料多 | 低,默认可用 | 自动生成 tx 日志表 | 中小企业默认选型,STARTER内置支持 |
| Narayana | 由某知名中间件团队维护 | 较高,依赖较多配置 | 需要手动配置对象存储 | 大型系统,已有JBoss/Narayana体系 |
| Bitronix | 维护基本停滞 | 中 | 自动生成,但文档少 | 历史遗留项目,不推荐新用 |
这套配置的取舍点在于:Atomikos 在 Spring Boot 生态里有现成的starter和自动配置,普通开发者不需要理解太多恢复原理就能跑通;Narayana胜在性能和恢复能力,但配置成本高,适合本来就熟悉它的人;Bitronix已经基本没有大版本更新,踩坑了只能自己看源码。除非你有明确的需求,否则不用折腾这一层。
2.4 XA能做到什么、做不到什么
XA能保证同一种资源类型(这里都是关系型数据库)的跨库操作强一致,并且对应用代码侵入最少,只要在事务边界打上注解,SQL不用改。这是它相比TCC最大的优势。
但也有三件事做不到:
第一,XA不适合跨异构资源。比如你要同时更新MySQL和Redis,XA的RM必须支持XA协议,Redis原生并不在XA语义里,此时强行做一致性,得自己封装RM或者换别的方案。
第二,XA是同步阻塞协议。全局事务期间,各库连接被持有,锁不释放,所以事务里的SQL执行时间、网络RT会直接放大成全局锁的等待时间。不能把长时间操作,比如调用外部接口、执行消息发送,放进XA事务里。
第三,XA的最终一致性依赖TM的事务日志。如果TM持久化了Prepare记录后崩溃,恢复时必须有人重放日志并驱动各RM提交或回滚。这个过程不是完全自动的,需要定期关注tx日志表和恢复机制。
理解这些边界之后,再回到工程配置会更有底气。
3. 环境准备与依赖选型:MyBatis-Plus和ShardingSphere怎么搭
3.1 版本矩阵:照着这个组合不会翻车
先给版本结论。我当前验证可用的组合是:
- Spring Boot 2.7.x
- ShardingSphere-JDBC 5.4.x(也就是 Sharding-JDBC 的 5.x 迭代版本)
- MyBatis-Plus 3.5.x
- mybatis-plus-boot-starter 配套
- MySQL 8.x(含InnoDB,XA要求表引擎支持事务)
这个组合不是唯一的,但它是目前踩坑最少、社区讨论最多的一个梯队。Spring Boot 3.x和JDK 17也是可用的,但国内存量项目很多还停留在Boot 2.7,相关兼容性问题相对可控。JDK版本建议17以上但不用上到21的预览特性,避免中间件偶发的反射兼容问题。
依赖引入要特别注意一个点:shardingsphere-jdbc-spring-boot-starter和mybatis-plus-boot-starter同时存在时,两者都会尝试管理数据源,很容易出现启动时数据源类型冲突。解决方式很统一:MyBatis-Plus的starter只负责SQL增强,你不需要手动给它指定DataSource;数据源统一交给ShardingSphere创建,MyBatis-Plus通过@MapperScan扫描Mapper接口时,使用的数据源实际上是ShardingSphere包装后的分片数据源。
在pom.xml中建议按下面结构引入:
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-spring-boot-starter</artifactId> <version>5.4.1</version> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-transaction-xa-core</artifactId> <version>5.4.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-extension</artifactId> <version>3.5.7</version> </dependency>shardingsphere-transaction-xa-core会默认把Atomikos相关类拉进依赖树,所以无需再单独引入Atomikos的包。如果你是极度精简依赖洁癖患者,可以只引starter然后手动排除掉不需要的事务管理器依赖,但正常情况下没必要。
3.2 搭建一个可复现的分片配置
在application.yml里,我们简化但完整地配置两个分片库:ds0、ds1。订单表逻辑表名t_order,按订单ID取模分到两个库,每个库里都有同样结构的物理表t_order。
spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/order_ds0?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/order_ds1?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order table-strategy: standard: sharding-column: order_id sharding-algorithm-name: order_id_hash_mod sharding-algorithms: order_id_hash_mod: type: HASH_MOD props: sharding-count: 2 props: sql-show: true这里有几个容易出错的地方:jdbc-url不是Spring Boot默认的url键名;actual-data-nodes写法里的$->{0..1}是老格式,在5.x里建议直接用ds0.t_order,ds1.t_order,避免yaml解析时美元符号被当模板渲染;sharding列必须指定,否则order_id作为分片键时,INSERT语句不知道往哪个库去。
sql-show: true建议在联调环境打开,它会把你看到的逻辑SQL和真正路由执行到哪个库的物理SQL都打印出来。后面验证XA事务有没有切到全局模式,这一步日志很有参考价值。
3.3 最容易犯的错:自己手动构造DataSource
这是我在几套项目改造里看到频率最高的错误:开发者在配置里自己定义了一个DataSource的@Bean,然后在里面用DriverManagerDataSource或者 DruidDataSource 手动创建连接,再传给Mapper使用。表面上看服务能启动、简单CRUD也正常,但一旦在方法上加上XA事务注解,启动就可能直接报No qualifying bean of type 'DataSource'或者事务管理器找不到连接。
原因在于ShardingSphere通过自动配置创建了一个ShardingSphere数据源,它会包装所有真实分片库的连接,并在事务上下文里做路由和事务增强。如果你手动创建了DataSource,Spring的自动配置就会被覆盖,ShardingSphere的XA事务管理器无法接管这个数据源的连接。正确的做法是:不定义数据源Bean,所有连接都由ShardingSphere创建,MyBatis-Plus的Mapper自动从Spring容器里拿这个分片数据源使用。
提示:把
spring.shardingsphere.enabled=true显式写上,再检查启动日志中是否出现“ShardingSphere props loaded”之类字样。如果没出现,多半是配置没被加载。
4. 跑通核心链路:XA事务+MyBatis-Plus的代码实现
4.1 事务类型怎么从本地切到XA
ShardingSphere 提供两种方式标记全局XA事务。
第一种,注解方式。在Service方法上同时加Spring的@Transactional和ShardingSphere的@ShardingSphereTransactionType(TransactionType.XA):
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.springframework.transaction.annotation.Transactional; @Service public class OrderServiceImpl { @Transactional @ShardingSphereTransactionType(TransactionType.XA) public void createOrderAcrossDatabases(OrderDO order) { // 这里面的SQL会走XA全局事务 } }第二种,编程式方式。在方法内部动态切换事务类型:
TransactionTypeHolder.set(TransactionType.XA); try { // 业务逻辑 } finally { TransactionTypeHolder.clear(); }编程式适合动态决定全局事务类型的场景,比如同一个Service方法在某些入口需要XA、某些入口只需要本地事务。但是要注意,TransactionTypeHolder是线程绑定变量,切换后必须在finally里清理,否则下一次复用线程时事务类型会被错误继承。
这里解释一个关键机制:@Transactional负责Spring事务边界的开启和提交,但ShardingSphere会通过内部的ShardingSphereTransactionManager识别当前ThreadLocal里的事务类型,然后选择对应实现。只有两个注解都到位,XA管理器才会参与,只加Spring的事务注解,走的依旧是本地事务逻辑。
4.2 一个跨库下单的完整代码示例
用MyBatis-Plus的IService和BaseMapper来写数据层,写一个必须在两个分片库同时成功的场景。假设t_order在ds0库,t_order_item也配置了分片规则可能在ds1库;同时涉及库存扣减,库存表也在分片范围内。为简洁,重点给出Service层的骨架和Mapper的基础结构。
public interface OrderMapper extends BaseMapper<OrderDO> { } public interface OrderItemMapper extends BaseMapper<OrderItemDO> { } public interface StockMapper extends BaseMapper<StockDO> { }对应的实体类,MyBatis-Plus注解与ShardingSphere逻辑表名保持一致:
@TableName("t_order") public class OrderDO { @TableId(type = IdType.INPUT) private Long orderId; private Long userId; private BigDecimal amount; private Integer status; } @TableName("t_order_item") public class OrderItemDO { private Long itemId; private Long orderId; private Long productId; private Integer quantity; }注意分片键orderId建议用INPUT方式自己赋值,而不是用MyBatis-Plus默认的雪花ID。雪花ID在单库场景完全没问题,但分片键如果不参与SQL,ShardingSphere无法在插入时确定目标库,会出现全库广播或直接无法路由。实际项目中我见过有人因为这个排查了一整天,最后发现是ID生成策略掩盖了分片键。
核心服务代码如下:
@Service @RequiredArgsConstructor public class OrderService { private final OrderMapper orderMapper; private final OrderItemMapper orderItemMapper; private final StockMapper stockMapper; @Transactional @ShardingSphereTransactionType(TransactionType.XA) public void createOrder(OrderCommand command) { long orderId = command.getOrderId(); OrderDO order = new OrderDO(); order.setOrderId(orderId); order.setUserId(command.getUserId()); order.setAmount(command.getAmount()); order.setStatus(0); orderMapper.insert(order); OrderItemDO item = new OrderItemDO(); item.setItemId(orderId * 10 + 1); item.setOrderId(orderId); item.setProductId(command.getProductId()); item.setQuantity(command.getQuantity()); orderItemMapper.insert(item); stockMapper.deductStock(command.getProductId(), command.getQuantity()); } }这段代码里三个Mapper对应三张不同的逻辑表,ShardingSphere会根据各自的逻辑表分片规则把SQL路由到不同分片库。XA事务管理器会收集所有参与了当前全局事务的库连接,并确保它们在第二阶段统一提交或回滚。其中stockMapper.deductStock是一个自定义的更新SQL,不是BaseMapper提供的方法,此时在Mapper接口里加@Update注解即可,ShardingSphere对这类自定义SQL的改写同样生效。
4.3 用手动异常验证回滚是否真的生效
代码写完之后,先别急着接真实业务,用最粗暴的方式验证:在业务代码中间人为抛异常。比如在orderItemMapper.insert(item)之后故意抛一个RuntimeException。
@Transactional @ShardingSphereTransactionType(TransactionType.XA) public void createOrderAndFail(OrderCommand command) { // 插入订单主表,会路由到ds0 orderMapper.insert(buildOrder(command)); // 模拟下游库/服务异常 throw new RuntimeException("mock fail for XA rollback test"); }执行这个测试方法后,去两个数据源分别查t_order表,预期结果是两个库都没有新增记录。如果只有ds0回滚而ds1没有,先检查是不是连接配置了autocommit=true,或者MyBatis-Plus的insert被单独开了新事务。更常见的现象是:执行后ds0和ds1各多一条数据,说明XA没有生效,代码走的还是本地事务。
确认XA事务生效,可以从运行时日志找突破口:开启sql-show后,当全局XA事务开启时,ShardingSphere日志中会出现包含“TransactionType.XA”或“XA transaction”相关字样的信息。同时,在数据库里执行:
SELECT * FROM information_schema.INNODB_TRX;在事务运行期间,你应能观察到两个库连接上都有一个处于RUNNING状态的事务。事务结束后再去查,应该一个都看不到。
另一个更靠谱的观测方法:进入Atomikos的事务日志目录(默认当前工作目录下),执行过程中会持续生成以tx为前缀的日志文件,格式类似tx-jdbc_..._xa_...。事务回滚后这些文件内容会发生变化。观察这个目录能直观看到全局事务的生成和销毁。
5. 实测避坑:这五个问题我挨个踩过
5.1 @Transactional不生效的第一排查思路
XA事务最常见的问题就是一加注解不生效,或者回滚只生效一半。注意下面这个排查链路的顺序,不用从数据源开始查,那绕远路。
第一步,确认@ShardingSphereTransactionType是否真的加载。这个注解属于ShardingSphere的事务模块,光引了shardingsphere-jdbc-spring-boot-starter而不引入shardingsphere-transaction-xa-core,注解会被识别但底层没有XA管理器,静默降级成本地事务。此时日志里不会报错,数据不会回滚,特别迷惑。
第二步,确认@Transactional没有被内部自调用绕过。Spring事务AOP的经典问题:this.createOrderAndFail()在同类内部被调用时,@Transactional会被跳过。排查时可以在Controller层注入Service后调用,或者用AopContext暴露代理对象。很多人把XA不生效误判为配置问题,实际上方法入口写错了。
第三步,确认没有在事务方法内部捕获异常。@Transactional默认回滚条件是抛出RuntimeException或Error,如果你在Service内部用try-catch把异常吞掉,方法正常返回,事务自然提交。XA全局事务也一样。调试阶段建议把catch里加上throw new RuntimeException(e)。
第四步,查看连接池是否配置了autoCommit。HikariCP默认autoCommit=true不会影响Spring事务,因为事务管理器会在开始时关闭自动提交;但如果你的分片库连接被Druid或者自定义拦截器干扰,事务边界就会失真。保持默认配置,不要手动调这个选项。
5.2 MyBatis-Plus分页插件在分片下的兼容
MyBatis-Plus的PaginationInnerInterceptor和ShardingSphere 5.x整体兼容,但有一个细节容易出问题:MP的IPage参数在分页之前需要先经过ShardingSphere的改写器,如果分页插件和分片改写顺序错乱,会导致COUNT查询被路由到了多个分片进行全库扫描,或者分页参数被错误改写。
实测下来的正确处理方式是:MyBatis-Plus的分页插件正常注册,但分页的limit值不要设置太大。ShardingSphere会对limit做改写,比如LIMIT 10, 10在四个分片上可能被改写成LIMIT 0, 20,然后内存归并后再分页。这个机制本身没问题,但如果你把PaginationInnerInterceptor的maxLimit设置过小,比如只允许单页100条,它会先于ShardingSphere的归并逻辑拦截SQL,报“超出最大页大小”错误,跟分片没有直接关系,但排查起来会先怀疑分片配置。
所以遇到MP分页在分片环境下的怪异行为时,先在非分片环境跑一遍同一个Mapper,确认分页插件本身逻辑正常,再进入分片环境下分析SQL改写日志。
5.3 Atomikos XA日志表无节制增长
Atomikos默认会在相对路径下生成事务日志文件,以及对应数据库中的日志表。长时间运行XA事务后,日志目录里会积累大量以tx_开头的文件。这些是事务恢复机制的核心数据,不能随便删除,但也不能放任不管。
在实际环境中我见过日志文件堆积导致磁盘打满的情况。处理方式是配置Atomikos定期清理已经结束的日志。在spring配置里加上:
spring: shardingsphere: props: xa-transaction-manager-type: Atomikos atomikos: max-timeout: 30000 default-timeout: 15000 max-actives: 50 enable-logging: trueenable-logging: true会生成日志;如果系统对恢复要求不高、业务允许极小概率的不一致,可以折中设为false来减少IO开销。但对于订单这类核心数据,建议保持true,并定期把日志目录备份到持久化存储。
实际还建议做一个定时任务,清理超过一定时间的tx文件。注意,Atomikos日志文件并不是事务一结束就立刻消失,它会保留一段时间兜底恢复。至少要等全局事务所有分支确认结束之后,才能安全清理。
5.4 XA事务性能为什么慢,怎么补偿
XA事务慢的原因主要在两块:本地事务基础上的Prepare阶段增加了通信轮次,以及全局事务期间所有参与的数据库连接一直被持有、行锁不释放。如果你在XA事务里执行了跨分片的查询,每个分片查询都会占用连接,并发量一上来,连接池很容易被打满。
补偿策略我一般这么组合:
第一,控制事务粒度和时间。XA事务里只放真正的写操作,读操作尽量放到事务外,或者把读操作降级为允许读旧数据。不能在一个XA事务里既调外部HTTP接口,又写六个库的表。
第二,调整Hikari连接池大小。全局事务期间连接不能被复用,所以连接池最大连接数不能只看日常QPS的算数平均值,要考虑全局事务占用的时间。经验值可以按同时并发事务数乘以每个事务参与的分片库数量来估算下限。
第三,给关键表设计好索引。XA第二阶段的锁等待,很多时候不是XA协议本身慢,而是慢SQL把锁持有时间拉长了。先把EXPLAIN调通,往往能解决很大一部分所谓的“XA很慢”问题。
我自己实测过,在一个写并发中等的交易系统里,如果单库只用@Transactional,方法平均RT是20ms左右;切到XA之后,同一方法平均RT可能到50ms以上。在订单场景这个代价通常可以接受,但如果你的接口本身是高频短事务,比如每秒上千次插入,XA就不适合了,建议考虑把这类操作拆分到普通本地事务,再用异步或事务消息做最终一致。
5.5 什么时候该放弃XA换别的事务方案
说了这么多,还是要给个诚实的判断:XA不是分布式事务的万能钥匙。
如果业务可以容忍短时间的账目不一致,比如余额变动后允许最终一致,那么选择本地消息表加消息队列通常成本更低、吞吐更高。如果每个参与方都需要实现Try和Confirm/Cancel,而且你们团队有足够的业务改造时间,TCC能把性能做得比XA好很多,但开发量也直线上升,维护成本不低。
我个人的选型倾向是:强一致需求、参与方少(2到4个)、每个库都是关系型数据库、事务链路短——直接上XA,这是最简单可靠的选择。参与方多、链路长、有外部非数据库资源参与——不要硬上XA,转消息最终一致。这个原则基本能覆盖大多数分库分表后的实际业务。
项目做到这一篇,ShardingSphere-JDBC的分片、读写分离、事务三块大能力已经串起来了。XA事务从理解原理到联调跑通,真正费时间的其实不是配置本身,而是遇到回滚不生效时如何快速定位到是缺依赖、还是自调用、还是异常被吞。这一套经验沉淀下来之后,下次再遇到“分库分表后数据不一致”的报障,你会非常自然地先去查全局事务是否真的被触发,而不是怀疑SQL写错或者数据源配错。