人人都会AI编程

10.6 多数据源事务与分布式事务基础方案

更新时间:2026-07-10

随着业务系统的演进,单一数据库往往难以满足性能与隔离性的双重需求。读写分离、分库分表、独立业务域拆分等架构策略会引入多个数据源,这就将一个尖锐的问题摆在了开发者面前:如何保证跨数据源的操作同时成功或同时失败? 本节将直击多数据源事务的挑战,给出两种主流应对方案:基于 JTA 的 XA 事务实现本地多数据源强一致性,以及面向微服务的最终一致性策略集。

10.6.1 多数据源带来的事务难题

在 Spring 中,当我们只配置一个数据源时,事务管理几乎无感——DataSourceTransactionManager 会从当前数据源获取连接,并通过 ThreadLocal 将连接绑定到事务中,所有 DAO 操作共享该连接,commitrollback 自然生效。

一旦引入多个数据源,情况立刻复杂化:

@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 等为微服务柔性事务铺平道路。理解原理,审视业务,方能选出最合适的方案。