随着业务系统的演进,单一数据库往往难以满足性能与隔离性的双重需求。读写分离、分库分表、独立业务域拆分等架构策略会引入多个数据源,这就将一个尖锐的问题摆在了开发者面前:如何保证跨数据源的操作同时成功或同时失败? 本节将直击多数据源事务的挑战,给出两种主流应对方案:基于 JTA 的 XA 事务实现本地多数据源强一致性,以及面向微服务的最终一致性策略集。
10.6.1 多数据源带来的事务难题
在 Spring 中,当我们只配置一个数据源时,事务管理几乎无感——DataSourceTransactionManager 会从当前数据源获取连接,并通过 ThreadLocal 将连接绑定到事务中,所有 DAO 操作共享该连接,commit 或 rollback 自然生效。
一旦引入多个数据源,情况立刻复杂化:
@Service
public class TransferService {
@Autowired private AccountMapper accountMapper; // 使用数据源 A
@Autowired private AuditLogMapper auditLogMapper; // 使用数据源 B
@Transactional // 默认绑定一个事务管理器,只能控制一个数据源
public void transfer(Long fromId, Long toId, BigDecimal amount) {
accountMapper.debit(fromId, amount); // 数据源 A
accountMapper.credit(toId, amount); // 数据源 A
auditLogMapper.insert(new AuditLog(...)); // 数据源 B —— 不在同一事务内!
}
}
如果 @Transactional 仅指定了针对数据源 A 的事务管理器,那么 auditLogMapper 的插入将不受事务控制,一旦其后发生异常,数据源 A 回滚而数据源 B 的提交已无法挽回。即便为每个数据源配置独立的 DataSourceTransactionManager,@Transactional 也只能绑定其中一个,无法让两个管理器协同工作。
这就要求我们引入分布式事务机制来协调多个资源。
10.6.2 本地多数据源的刚性事务:JTA + XA 二阶段提交
对于部署在同一个应用内的多数据源(例如同库异构表使用不同数据源,或者同应用的业务库与日志库),最直接的解决手段是使用支持 XA 协议 的 JTA 全局事务。
XA 二阶段提交原理
XA 定义了事务管理器(TM)与资源管理器(RM,即数据库)之间的接口。协调过程分两个阶段:
- 准备阶段:TM 向所有参与事务的 RM 发出 prepare 指令。每个 RM 执行事务操作并记录 undo/redo 日志,但不提交,然后回复“就绪”或“失败”。
- 提交阶段:若所有 RM 都返回就绪,TM 发送 commit 指令让各 RM 提交;若任一 RM 返回失败,TM 发送 rollback 指令让所有 RM 回滚。
实战:Spring Boot + Atomikos 配置 JTA 全局事务
Atomikos 是一个轻量级的 JTA 事务管理器实现,内嵌使用非常简便,且对 Spring 有良好集成。以下演示如何配置两个 XA 数据源。
步骤 1:引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
步骤 2:配置 XA 数据源与 JTA 事务管理器
Spring Boot 的 JtaAutoConfiguration 已自动配置好 JtaTransactionManager,我们只需提供两个数据源的 Bean,并显式使用 XA 数据源类(例如 MySQL 的 MysqlXADataSource)。
@Configuration
public class XADataSourceConfig {
@Bean("dataSourceA")
@ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.a")
public DataSource dataSourceA() {
return new AtomikosDataSourceBean(); // 自动装配,绑定XA数据源
}
@Bean("dataSourceB")
@ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.b")
public DataSource dataSourceB() {
return new AtomikosDataSourceBean();
}
}
对应 application.yml 配置:
spring:
jta:
atomikos:
datasource:
a:
xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
xa-properties:
url: jdbc:mysql://localhost:3306/account_db?useSSL=false
user: root
password: 123456
max-pool-size: 20
unique-resource-name: dsA
b:
xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
xa-properties:
url: jdbc:mysql://localhost:3306/audit_db?useSSL=false
user: root
password: 123456
max-pool-size: 20
unique-resource-name: dsB
步骤 3:在 Service 中使用 @Transactional
无需额外配置,Spring 自动使用 JtaTransactionManager。现在 TransferService 的方法若抛出异常,两个数据源将整体回滚。
@Service
public class TransferService {
@Autowired @Qualifier("accountJdbcTemplate") private JdbcTemplate accountJdbc;
@Autowired @Qualifier("auditJdbcTemplate") private JdbcTemplate auditJdbc;
@Transactional
public void transfer(Long from, Long to, BigDecimal amount) {
accountJdbc.update("UPDATE account SET balance = balance - ? WHERE id = ?", amount, from);
accountJdbc.update("UPDATE account SET balance = balance + ? WHERE id = ?", amount, to);
auditJdbc.update("INSERT INTO audit_log(operation, amount) VALUES (?,?)", "TRANSFER", amount);
if (amount.compareTo(new BigDecimal("10000")) > 0) {
throw new RuntimeException("模拟异常,触发全局回滚");
}
}
}
注意事项
- 数据库本身需要支持 XA:MySQL 的 InnoDB 引擎支持 XA,但需确保
xa-data-source-class配置正确。 - 性能代价:XA 的两阶段提交会带来额外通信开销和锁持有时间,吞吐量会下降 30%~50%,须谨慎评估。
- 悬疑事务:在 prepare 后 TM 宕机,未完成的 XA 事务会残留在数据库中,需人工干预或使用自动恢复机制。Atomikos 提供了内置的日志和恢复功能。
10.6.3 面向微服务的最终一致性方案
当多数据源跨越不同微服务、甚至不同数据库类型时,XA 的全局锁会严重拖累系统可用性。微服务架构更倾向最终一致性,通过以下模式实现。
| 模式 | 核心思想 | 一致性 | 复杂度 | 适用场景 |
|------|---------|--------|--------|----------|
| TCC (Try/Confirm/Cancel) | 业务分三阶段:预留资源(Try)、确认提交(Confirm)、补偿回滚(Cancel) | 最终一致 | 高(需实现三个接口) | 资金转账、库存扣减等对一致性要求较高的场景 |
| Saga | 将长事务拆分为一系列本地事务,每个事务有对应的补偿操作。失败时逆序执行补偿 | 最终一致 | 中 | 订单流程、旅行预订等多步骤流程 |
| 可靠消息驱动 | 本地事务与消息发送绑定(如事务消息表或 RocketMQ 事务消息),消费端做幂等处理 | 最终一致 | 中 | 异步通知、数据同步等 |
| AT 模式(Seata) | 基于数据库代理自动生成回滚 SQL,类似 XA 但阶段更轻量 | 最终一致 | 低(业务零侵入) | 希望像本地事务一样编程,又不堪 XA 性能 |
以 Seata AT 模式 为例,业务代码甚至可以直接使用 @GlobalTransactional 注解:
@GlobalTransactional
public void crossServiceTransfer() {
accountService.debit(...); // 远程调用
inventoryService.reduceStock(...); // 远程调用
}
Seata 会在各服务的数据源上代理 SQL,记录前镜像后镜像,在提交时清理 undo,在回滚时自动生成反向 SQL,达到近乎透明的分布式事务体验。
10.6.4 实践建议:设计优于技术
没有银弹。在决定何时使用何种方案之前,一个更根本的策略是通过业务设计避免分布式事务:
- 将必须强一致性的数据放在同一个数据库、同一个微服务域内。
- 对于可容忍短时间不一致的场景,改用异步消息 + 补偿任务。
- 非核心数据(如日志、统计)可独立存储,使用非事务方式写入,事后依赖校对程序保证最终一致。
只有在业务确实无法拆分,且一致性要求极高时才启用 XA;否则优先使用最终一致性方案以换取性能和可用性。Spring 生态为每一层级都提供了可靠的集成支持:JTA 搞定本地多库刚性事务,Seata、RocketMQ 等为微服务柔性事务铺平道路。理解原理,审视业务,方能选出最合适的方案。