Druid 可以说是国内使用最广泛的数据源连接池产品

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.1.17</version>
</dependency>

application.yml配置

spring:
  datasource:
    pay:
      username: root
      password: root
      host: 192.168.37.133:3306
      jdbc-url: jdbc:mysql://${spring.datasource.pay.host}/${spring.datasource.pay.schema-name}?useUnicode=true&characterEncoding=utf8&useSSL=false
      schema-name: dlsms
    order:
      username: root
      password: root
      host: 192.168.37.134:3306
      jdbc-url: jdbc:mysql://${spring.datasource.order.host}/${spring.datasource.order.schema-name}?useUnicode=true&characterEncoding=utf8&useSSL=false
      schema-name: dlsms

方案一:手动切换数据源

适合没有分模块的功能,后期又需要加入多数据源

MyBatis-Plus配置类

作用:配置数据源和事务管理

import net.test.project.constants.DataSourceKey;

import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import net.test.project.constants.DataSourceKey;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;


@MapperScan({"net.test.project.dao.mapper.*"})
@Configuration
public class DataSourceProxyConfig {

    /**
     * 配置默认数据源
     *
     * @return
     */
    @Bean(name = "originOrder") // 声明其为Bean实例
    @ConfigurationProperties(prefix = "spring.datasource.order")
    public DataSource dataSourceOrder() {
        return new DruidDataSource();
    }

    @Bean(name = "originStorage") // 声明其为Bean实例
    @ConfigurationProperties(prefix = "spring.datasource.storage")
    public DataSource dataSourceStorage() {
        return new DruidDataSource();
    }

    @Bean(name = "originPay") // 声明其为Bean实例
    @ConfigurationProperties(prefix = "spring.datasource.pay")
    public DataSource dataSourcePay() {
        return new DruidDataSource();
    }

    /**
     * 设置多数据源,配置动态路由数据源
     *
     * @return
     */
    @Bean("dynamicDataSource")
    public DataSource dynamicDataSource(@Qualifier("originOrder") DataSource dataSourceOrder,
                                        @Qualifier("originStorage") DataSource dataSourceStorage,
                                        @Qualifier("originPay") DataSource dataSourcePay) {
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DataSourceKey.ORDER.name(), dataSourceOrder);
        dataSourceMap.put(DataSourceKey.STORAGE.name(), dataSourceStorage);
        dataSourceMap.put(DataSourceKey.PAY.name(), dataSourcePay);

        //设置默认
        DynamicDataSource dynamicRoutingDataSource = new DynamicDataSource();
        dynamicRoutingDataSource.setDefaultTargetDataSource(dataSourceOrder);
        dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);

        return dynamicRoutingDataSource;
    }

    /**
     * 设置会话工厂
     *
     * @return
     * @throws Exception
     */
    @Bean(name = "sqlSessionFactory")
    @Primary
    public SqlSessionFactory mybatisSqlSessionFactoryBean(
            @Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        // 设置别名扫描包(实体类所在包)
        sqlSessionFactoryBean.setTypeAliasesPackage("com.test.custom.entity");

        // 设置 Mapper XML 文件位置(与 application.yml 中配置一致)
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] mapperLocations = resolver.getResources("classpath*:mapper/*.xml");
        sqlSessionFactoryBean.setMapperLocations(mapperLocations);

        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);

        sqlSessionFactoryBean.setConfiguration(configuration);
        sqlSessionFactoryBean.setPlugins(mybatisPlusInterceptor());
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * 添加分页插件(可选)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }

    /**
     * 默认事务管理器配置,管理 DynamicDataSource
     *
     * <p>用于普通 {@code @Transactional} 注解,保持向后兼容。
     * 跨库事务请使用 {@code @MultiTransactional} 注解。
     * 使用 ConditionalOnMissingBean 避免与其他配置冲突。
     *
     * @return PlatformTransactionManager(@Primary)
     */
    @Bean
    @Primary
    @ConditionalOnMissingBean(name = "transactionManager")
    public PlatformTransactionManager txManager(@Qualifier("dataSources") DataSource multipleDataSource) {
        return new DataSourceTransactionManager(multipleDataSource);
    }

    /*
     * ============================================================
     * 以下为每个物理数据源创建独立的 TransactionManager
     * 用于 @MultiTransactional 跨库事务管理
     *
     * 注意:
     * 1. 这些 TransactionManager 直接绑定到物理 DataSource,而非 DynamicDataSource
     * 2. 它们不被 @Primary 标注,默认事务管理仍由 txManager 处理
     * 3. 它们被 MultiDataSourceTxManagerRegistry 集中管理和注入
     * ============================================================
     */
    @Bean("orderTransactionManager")
    public PlatformTransactionManager orderTransactionManager(@Qualifier("originOrder") DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }

    @Bean("storageTransactionManager")
    public PlatformTransactionManager storageTransactionManager(@Qualifier("originStorage") DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }

    @Bean("payTransactionManager")
    public PlatformTransactionManager payTransactionManager(@Qualifier("originPay") DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
}

由于是MyBatsi-Plus,所以配的是MybatisSqlSessionFactoryBean,如果是MyBatis,则应该是SqlSessionFactoryBean

实现选择目标数据源

import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * 动态数据源路由
 *
 * <p>基于 Spring {@link AbstractRoutingDataSource} 实现多数据源切换。
 * 重写 {@link #getConnection()} 方法,在获取连接时通过
 * {@link DataSourceUtils#getConnection(DataSource)} 获取,确保能够感知
 * Spring 事务管理器(包括 {@code @MultiTransactional})已绑定的连接资源。
 *
 * <p>原 {@code AbstractRoutingDataSource.getConnection()} 直接调用
 * {@link DataSource#getConnection()},会绕过
 * {@link org.springframework.transaction.support.TransactionSynchronizationManager}
 * 中已绑定的事务连接,导致 MyBatis 操作与事务管理器使用不同的物理连接,致使跨库事务失效。
 *
 * @author huangtao
 * @since 2019/7/24
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 取得当前使用哪个数据源
     *
     * @return 当前线程绑定的数据源 lookup key
     */
    @Override
    protected Object determineCurrentLookupKey() {
        log.info("当前数据源 [{}]", DynamicDataSourceContextHolder.getDataSourceType());
        return DynamicDataSourceContextHolder.getDataSourceType();
    }

    /**
     * 获取数据库连接
     *
     * <p>先解析当前线程对应的目标物理 DataSource,再通过
     * {@link DataSourceUtils#getConnection(DataSource)} 获取连接。
     * 这样当 {@code MultiTransactionManager} 已将目标 DataSource 的
     * 连接绑定到 {@code TransactionSynchronizationManager} 时,
     * 会直接复用该事务连接,而非从连接池获取新连接。
     *
     * @return 参与当前 Spring 事务的连接(如有),否则为连接池新连接
     * @throws SQLException 获取连接失败时抛出
     */
    @Override
    public Connection getConnection() throws SQLException {
        DataSource targetDataSource = determineTargetDataSource();
        return DataSourceUtils.getConnection(targetDataSource);
    }
}

数据源字典

public enum DataSourceKey {

  ORDER("order", "order数据库"),
  // 定义数据源类型为BASIC,表示基础数据库
  STORAGE("storage", "storage数据库"),
  // 定义数据源类型为BILL,表示账单计费数据库
  PAY("pay", "计费数据库"),
  ;

  // 数据源的类型标识
  private String type;
  // 数据源的名称
  private String name;

  /**
   * 构造函数,初始化数据源类型和名称
   *
   * @param type 数据源的类型标识
   * @param name 数据源的名称
   */
  DataSourceKey(String type, String name) {
    this.type = type;
    this.name = name;
  }

  /**
   * 获取数据源的类型标识
   *
   * @return 数据源的类型标识
   */
  public String getType() {
    return type;
  }

  /**
   * 设置数据源的类型标识
   *
   * @param type 数据源的类型标识
   */
  public void setType(String type) {
    this.type = type;
  }

  /**
   * 获取数据源的名称
   *
   * @return 数据源的名称
   */
  public String getName() {
    return name;
  }

  /**
   * 设置数据源的名称
   *
   * @param name 数据源的名称
   */
  public void setName(String name) {
    this.name = name;
  }

  /**
   * 根据类型标识获取对应的枚举值
   *
   * @param type 数据源的类型标识
   * @return 对应类型标识的枚举值,如果找不到匹配的,则返回NULL
   */
  public static DataSourceKey getEnumByType(String type) {
    // 默认返回值为NULL,即无数据源
    DataSourceKey e = NULL;

    // 如果传入的类型不为空,则遍历所有枚举值寻找匹配的类型
    if (type != null) {
      for (DataSourceKey o : values()) {
        // 如果找到匹配的类型,则更新返回值并终止遍历
        if (o.getType().equals(type)) {
          e = o;
          break;
        }
      }
    }

    // 返回找到的枚举值
    return e;
  }

}

切换数据源

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DynamicDataSourceContextHolder {

  private static final ThreadLocal contextHolder = new ThreadLocal<>();

  private static final ThreadLocal afterContextHolder = new ThreadLocal<>();

  public static void setAfterDbType(DataSourceKey dbTypeEnum) {
    afterContextHolder.set(dbTypeEnum.getType());
  }

  public static String getAfterDbType() {
    return (String) afterContextHolder.get();
  }

  public static void clearAfterDbType() {
    afterContextHolder.remove();
  }

  /**
   * 设置数据源
   *
   * @param dbTypeEnum
   */
  public static void setDataSourceType(DataSourceKey dbTypeEnum) {
    log.info("切换到{}数据源", dbTypeEnum.getName());
    contextHolder.set(dbTypeEnum.getType());
  }

  /**
   * 取得当前数据源
   *
   * @return
   */
  public static String getDataSourceType() {
    return (String) contextHolder.get();
  }

  /**
   * 清除上下文数据
   */
  public static void clearDataSourceType() {
    contextHolder.remove();
  }
}

使用方法:查前选库

DynamicDataSourceContextHolder.setDataSourceType(DataSourceKey.PAY);

多数据源下的事务问题

多数据源下使用事务时,需要在注解上明确是哪个数据源,类似下面这样,否则会报找不到事务管理实例的错误。

@Transactional(value = "transactionManager",rollbackFor = Exception.class)

方案二:

自动数据源切换

组织两个配置类。原理:mybatis多数据源的原理是根据不同包,调用不同的数据源,你只需要把你的mapper.java和mapper.xml写在某个package中,springboot自动帮你实现数据源切换。注意第二个配置类不需要@Primary

  • @MapperScan注解中的basePackages指向的是指定的Dao层。
  • @MapperScan注解中sqlSessionFactoryRef 用来指定使用某个SqlSessionFactory来操作数据源。
  • setMapperLocations指向的是操作执行数据库的Mapper层。
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

@Configuration
@MapperScan(basePackages = "com.example.dao.crm", sqlSessionTemplateRef = "crmSqlSessionTemplate")
public class CrmDataSourceConfig {

  @Bean(name = "crmDataSource")
  @ConfigurationProperties(prefix = "spring.datasource.crm")
  public DataSource crmDataSourceConfig() {
    return new DruidDataSource();
  }


  @Bean(name = "crmSqlSessionFactory")
  @Primary
  public SqlSessionFactory crmSqlSessionFactory(@Qualifier("crmDataSource") DataSource dataSource) throws Exception {
    MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
    sqlSessionFactory.setDataSource(dataSource);
    sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/crm/*.xml"));

    MybatisConfiguration configuration = new MybatisConfiguration();
    configuration.setJdbcTypeForNull(JdbcType.NULL);
    configuration.setMapUnderscoreToCamelCase(true);
    configuration.setCacheEnabled(false);
    sqlSessionFactory.setConfiguration(configuration);
    sqlSessionFactory.setPlugins(mybatisPlusInterceptor());
    
    return sqlSessionFactory.getObject();
  }
  
  /**
   * 添加分页插件(可选)
   */
  @Bean
  public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
    return interceptor;
  }
  
  @Bean(name = "crmTransactionManager")
  @Primary
  public DataSourceTransactionManager crmTransactionManager(@Qualifier("crmDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
  }

  @Bean(name = "crmSqlSessionTemplate")
  @Primary
  public SqlSessionTemplate crmSqlSessionTemplate(@Qualifier("crmSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
  }
}

切面自动切换

通过切面的包路径配置实现

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Aspect
@Order(-10000) // 这是为了保证AOP在事务注解之前生效,Order的值越小,优先级越高
@Slf4j
public class DataSourceSwitchAspect {

  @Pointcut("execution(* net.est.test.api.service.order..*.*(..))")
  private void db3Aspect() {
  }


  @Pointcut("execution(* net.est.test.api.service.pay..*.*(..))\"")
  private void db5Aspect() {
  }


  @Before("db3Aspect()")
  public void rpt() {
    final String afterDbType = DynamicDataSourceContextHolder.getDataSourceType();
    final DataSourceKey afterDB = DataSourceKey.getEnumByType(afterDbType);
    DynamicDataSourceContextHolder.setAfterDbType(afterDB);
    log.debug("切换到{}数据源...", DataSourceKey.ORDER.name());
    DynamicDataSourceContextHolder.setDataSourceType(DataSourceKey.ORDER);
  }

  @Before("db5Aspect()")
  public void smsConfig() {
    final String afterDbType = DynamicDataSourceContextHolder.getDataSourceType();
    final DataSourceKey afterDB = DataSourceKey.getEnumByType(afterDbType);
    DynamicDataSourceContextHolder.setAfterDbType(afterDB);
    log.debug("切换到{}数据源...", DataSourceKey.PAY.name());
    DynamicDataSourceContextHolder.setDataSourceType(DataSourceKey.PAY);
  }

  @After("db3Aspect()||db5Aspect()")
  public void back() {
    final String afterDbType = DynamicDataSourceContextHolder.getAfterDbType();
    log.debug("切回到{}数据源...", afterDbType);
    if (StringUtils.isEmpty(afterDbType)) {
      DynamicDataSourceContextHolder.setDataSourceType(DataSourceKey.PAY);
    } else {
      final DataSourceKey afterByType = DataSourceKey.getEnumByType(afterDbType);
      DynamicDataSourceContextHolder.setDataSourceType(afterByType);
    }
  }
}

定义了一个基于 AOP 的数据源切换切面类 DataSourceSwitchAspect,该切面用于实现动态数据源切换,支持在不同业务模块访问不同数据库。其功能如下:
定义切点:分别针对 order 和 pay 包下的方法定义切点 db3Aspect 和 db5Aspect。
前置通知:

  • 在执行 order 包方法前,将数据源切换为 ORDER。
  • 在执行 pay 包方法前,将数据源切换为 PAY。

后置通知:在目标方法执行结束后,将数据源恢复为之前的状态。

方案三:推荐使用@DS

在 pom.xml 中引入 dynamic-datasource-spring-boot-starter 依赖:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.1</version> //选择合适的版本
</dependency>

在 application.yml 中配置多个数据源。以下是一个示例配置:

spring:
  datasource:
    dynamic:
      primary: master # 默认数据源
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=UTC
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave:
          url: jdbc:mysql://localhost:3306/slave_db?useSSL=false&serverTimezone=UTC
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver

数据源切换

dynamic-datasource-spring-boot-starter 提供了 @DS 注解,这个注解可以直接用于 Service 层的方法、Mapper 接口的方法,甚至整个 Mapper 类上。

import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.stereotype.Service;
@Service
public class UserService {
    @DS("master") // 使用 master 数据源
    public void addUser(User user) {
        // 插入用户数据
    }

    @DS("slave") // 使用 slave 数据源
    public User getUserById(Long id) {
        // 查询用户数据
        return userMapper.selectById(id);
    }
}

根据您提供的 application.yml 配置文件中 spring.datasource.dynamic.datasource 下的配置
@DS("master") 很可能对应的是master数据源。

它简化了配置和切换逻辑,通过引入依赖,配置数据源和使用@DS注解,可以轻松实现数据源的动态切换

常见问题

druid抛出异常:javax.management.InstanceAlreadyExistsException: com.alibaba.druid:type=DruidDataSource,id=xxx

同一个domain里面的MBean要求name唯一,检查SpringBoot应用的application.properties等配置文件name配置

实现跨数据库事务

@Transactional注解只能控制单个数据源内的事务。要实现跨数据库事务,可以考虑以下几种方案:

方案一:使用JTA + Atomikos

JTA+Atomikos实现分布式事务-CSDN博客

方案二:使用消息队列实现最终一致性

对于性能要求较高、可以接受最终一致性的场景,可以考虑使用消息队列实现:

  • 在主数据库执行操作并提交事务
  • 发送消息到消息队列
  • 消费者从消息队列获取消息,在其他数据库执行相应操作

方案三:Seata AT 模式(分布式事务框架)

需要部署 Seata Server(新基础设施)

方案四:TCC模式

对于复杂业务场景,可以使用TCC模式:

  • Try阶段:预留资源
  • Confirm阶段:确认操作
  • Cancel阶段:取消操作

方案五:自研分布式事物

  • 优点:零新依赖,仅Spring原生能力。可配置化,高内聚低耦合
  • 缺陷:MultiTransactionManager.commit() 按 CCOD → BMS → ACCOUNT 顺序提交,如果 BMS 提交失败,CCOD 已提交无法回滚,数据就永远不一致了
┌──────────────────────────────────────────────────┐
│  @MultiTransactional({CCOD, BMS, ACCOUNTCENTER}) │
│                                                   │
│  MultiTransactionManager                          │
│  ├── CCOD TxManager → TransactionStatus 1         │
│  ├── BMS TxManager  → TransactionStatus 2         │
│  └── ACCOUNT TxManager → TransactionStatus 3      │
│                                                   │
│  关键:AOP 拦截 → 开启多个独立事务 → 统一提交/回滚   │
│  附加:支持配置化绑定(Service方法 → 数据源列表)     │
└──────────────────────────────────────────────────┘

改造文件列表

MyBatis 所有 Mapper 操作都经过 DynamicDataSource,这是连接复用的前提。

sqlSessionFactoryBean.setDataSource(dataSource);  // ← dataSource = DynamicDataSource 实例

DynamicDataSource.getConnection() 覆写让MyBatis与事务管理器使用同一个物理连接

MultiTransactional 注解

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 跨数据库事务注解
 *
 * <p>用于声明一个方法需要在多个数据源上执行事务操作。
 * 被标注的方法中的所有数据库操作将统一提交或回滚,
 * 遵循"Best Efforts 1PC"模式(非XA,极端断电场景有风险)。
 *
 * <p>设计模式:声明式事务管理 + AOP切面拦截
 *
 * <p>使用示例:
 * <pre>{@code
 * @MultiTransactional({DataSourceKey.ORDER, DataSourceKey.STORAGE, DataSourceKey.PAY})
 * public Object workConfigSave(BasicUserInfo basicUserInfo, AgentChangeNewDto agentChangeDto) {
 *     // 方法内部仍然使用 DynamicDataSourceContextHolder.setDataSourceType() 切换数据源
 *     // 事务由 MultiTransactionalAspect 统一管理提交和回滚
 * }
 * }</pre>
 *
 * <p>注意事项:
 * <ul>
 *   <li>方法内部仍需通过 {@code DbContextHolder.setDbType()} 切换MyBatis数据源路由</li>
 *   <li>不支持嵌套调用(同一线程中重复开启多个MultiTransactional)</li>
 *   <li>各数据源事务独立,提交顺序为数组声明顺序,回滚顺序为逆序</li>
 *   <li>与 {@code @Transactional} 互斥,不要同时使用</li>
 * </ul>
 *
 * @author team
 * @since 1.0
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiTransactional {

    /**
     * 参与事务的数据源类型列表
     *
     * <p>事务开启顺序与该数组声明顺序一致,
     * 提交顺序与声明顺序一致,回滚顺序为逆序。
     *
     * @return 数据源类型数组,至少包含一个数据源
     */
    DataSourceKey[] value();
}

MultiTransactionContext 事物状态

import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 跨库事务上下文
 *
 * <p>持有当前跨库事务中所有数据源的事务状态映射,
 * 由 {@link MultiTransactionManager} 创建和管理。
 *
 * <p>线程安全:当前设计为单线程使用(跟随方法调用栈),不跨线程共享。
 *
 * @author team
 * @since 1.0
 */
public class MultiTransactionContext {

    /**
     * 数据源类型 → (事务管理器, 事务状态) 的有序映射
     *
     * <p>使用 LinkedHashMap 保证开启顺序,回滚时使用逆序
     */
    private final LinkedHashMap<DataSourceKey, TransactionEntry> transactionMap;

    /**
     * 构造事务上下文
     *
     * @param initialCapacity 预期参与事务的数据源数量,用于避免Map扩容
     */
    MultiTransactionContext(int initialCapacity) {
        this.transactionMap = new LinkedHashMap<>(initialCapacity);
    }

    /**
     * 注册一个数据源的事务信息
     *
     * @param datasource 数据源类型
     * @param txManager  该数据源的事务管理器
     * @param txStatus   已开启的事务状态
     */
    void register(DataSourceKey datasource, PlatformTransactionManager txManager, TransactionStatus txStatus) {
        transactionMap.put(datasource, new TransactionEntry(txManager, txStatus));
    }

    /**
     * 获取所有事务条目按开启顺序
     *
     * @return 事务条目Map(只读视图)
     */
    Map<DataSourceKey, TransactionEntry> getTransactionMap() {
        return transactionMap;
    }

    /**
     * 获取已注册的数据源数量
     *
     * @return 数据源数量
     */
    int size() {
        return transactionMap.size();
    }

    /**
     * 事务条目:封装事务管理器和事务状态
     */
    static class TransactionEntry {
        private final PlatformTransactionManager txManager;
        private final TransactionStatus txStatus;

        TransactionEntry(PlatformTransactionManager txManager, TransactionStatus txStatus) {
            this.txManager = txManager;
            this.txStatus = txStatus;
        }

        PlatformTransactionManager getTxManager() {
            return txManager;
        }

        TransactionStatus getTxStatus() {
            return txStatus;
        }
    }
}

MultiTransactionManager 生命周期管理

import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 跨数据库事务管理器
 *
 * <p>核心职责:
 * <ol>
 *   <li>为多个数据源独立开启事务(每个数据源使用各自的 TransactionManager)</li>
 *   <li>统一提交所有事务(按声明顺序)</li>
 *   <li>统一回滚所有事务(按声明逆序)</li>
 * </ol>
 *
 * <p>实现原理:利用 Spring 的 {@code TransactionSynchronizationManager} 为
 * 不同 DataSource 绑定独立的 Connection 资源,实现同一线程中多个物理数据库
 * 的独立事务管理。
 *
 * <p>事务模式:Best Efforts 1PC(非 XA 分布式事务)
 * <ul>
 *   <li>优势:零额外依赖,性能好,适合绝大多数业务场景</li>
 *   <li>局限:极端宕机场景可能出现部分提交(概率极低)</li>
 * </ul>
 *
 * <p>使用方式:通过 {@code @MultiTransactional} 注解声明,由 {@code MultiTransactionalAspect} 自动调用。
 *
 * @author team
 * @since 1.0
 */
@Slf4j
@Component
public class MultiTransactionManager {

    private final MultiDataSourceTxManagerRegistry registry;
    private final ThreadLocal<MultiTransactionContext> currentContext = new ThreadLocal<>();

    public MultiTransactionManager(MultiDataSourceTxManagerRegistry registry) {
        this.registry = registry;
    }

    /**
     * 为指定的数据源列表开启跨库事务
     *
     * <p>按数组声明顺序依次为每个数据源开启独立事务。
     * 每个事务的传播行为为 PROPAGATION_REQUIRES_NEW,确保独立性。
     *
     * @param datasources 需要参与事务的数据源类型数组
     * @return 跨库事务上下文,包含所有数据源的事务状态
     * @throws IllegalStateException 如果当前线程已有未完成的跨库事务
     */
    public MultiTransactionContext begin(DataSourceKey... datasources) {
        if (currentContext.get() != null) {
            throw new IllegalStateException("当前线程已存在未完成的跨库事务,不支持嵌套调用");
        }
        if (datasources == null || datasources.length == 0) {
            throw new IllegalArgumentException("跨库事务至少需要指定一个数据源");
        }

        MultiTransactionContext context = new MultiTransactionContext(datasources.length);
        Exception firstException = null;
        int successCount = 0;

        for (DataSourceKey datasource : datasources) {
            try {
                PlatformTransactionManager txManager = registry.getTransactionManager(datasource);
                /*
                 * 切换 DynamicDataSourceContextHolder 确保 DataSourceTransactionManager
                 * 从正确的物理数据源获取连接
                 */
                DynamicDataSourceContextHolder.setDataSourceKey(datasource);
                DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
                definition.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRES_NEW);
                TransactionStatus txStatus = txManager.getTransaction(definition);
                context.register(datasource, txManager, txStatus);
                successCount++;
                log.debug("跨库事务开启成功: datasource={}, 已开启 {}/{}", datasource.getType(), successCount, datasources.length);
            } catch (Exception e) {
                log.error("跨库事务开启失败: datasource={}, 共开启 {} 个后发生异常", datasource.getType(), successCount, e);
                firstException = e;
                break;
            }
        }

        /*
         * 如果部分事务开启失败,需要回滚已开启的事务
         */
        if (firstException != null) {
            rollbackPartially(context, successCount);
            throw new RuntimeException("跨库事务开启失败,已回滚已开启的事务", firstException);
        }

        currentContext.set(context);
        return context;
    }

    /**
     * 提交当前跨库事务中的所有数据源事务
     *
     * <p>按注册顺序依次提交。如果某个数据源提交失败,
     * 已提交的数据源无法回滚,需记录严重错误日志并触发告警。
     *
     * @param context 跨库事务上下文
     */
    public void commit(MultiTransactionContext context) {
        if (context == null) {
            return;
        }
        ArrayList<DataSourceKey> committedList = new ArrayList<>();

        for (Map.Entry<DataSourceKey, MultiTransactionContext.TransactionEntry> entry : context.getTransactionMap().entrySet()) {
            DataSourceKey datasource = entry.getKey();
            MultiTransactionContext.TransactionEntry txEntry = entry.getValue();
            try {
                txEntry.getTxManager().commit(txEntry.getTxStatus());
                committedList.add(datasource);
                log.debug("跨库事务提交成功: datasource={}", datasource.getType());
            } catch (Exception e) {
                /*
                 * 提交失败的严重处理:
                 * 已提交的数据源无法回滚,这是 Best Efforts 1PC 的固有局限。
                 * 需要记录完整的上下文信息,便于后续人工补偿。
                 */
                log.error("跨库事务提交失败! datasource={}, 已提交的数据源={}, 剩余未提交的数据源待人工检查", datasource.getType(), committedList.stream().map(DataSourceKey::getType).toList(), e);
                throw new RuntimeException(String.format("跨库事务提交失败: datasource=%s, 已提交=%s", datasource.getType(), committedList.stream().map(DataSourceKey::getType).toList()), e);
            }
        }
    }

    /**
     * 回滚当前跨库事务中的所有数据源事务
     *
     * <p>按注册顺序的逆序回滚。每个数据源回滚时出现异常不会阻止后续回滚,
     * 但会记录错误日志。
     *
     * @param context 跨库事务上下文
     */
    public void rollback(MultiTransactionContext context) {
        if (context == null) {
            return;
        }
        /*
         * 转换为数组以便逆序遍历
         */
        List<Map.Entry<DataSourceKey, MultiTransactionContext.TransactionEntry>> entries = new ArrayList<>(context.getTransactionMap().entrySet());

        for (int i = entries.size() - 1; i >= 0; i--) {
            Map.Entry<DataSourceKey, MultiTransactionContext.TransactionEntry> entry = entries.get(i);
            DataSourceKey datasource = entry.getKey();
            MultiTransactionContext.TransactionEntry txEntry = entry.getValue();
            try {
                txEntry.getTxManager().rollback(txEntry.getTxStatus());
                log.debug("跨库事务回滚成功: datasource={}", datasource.getType());
            } catch (Exception e) {
                log.error("跨库事务回滚失败: datasource={}, 需人工检查该数据源状态", datasource.getType(), e);
            }
        }
    }

    /**
     * 清理当前线程的跨库事务上下文
     *
     * <p>必须在事务提交或回滚后调用,防止 ThreadLocal 内存泄漏。
     *
     * @param context 跨库事务上下文
     */
    public void cleanup(MultiTransactionContext context) {
        if (context != null) {
            currentContext.remove();
        }
    }

    /**
     * 获取当前线程的跨库事务上下文
     *
     * @return 当前上下文,如果不存在则返回 null
     */
    MultiTransactionContext getCurrentContext() {
        return currentContext.get();
    }

    /**
     * 回滚已部分开启的事务(用于开启过程中发生异常的场景)
     *
     * @param context      事务上下文
     * @param successCount 已成功开启的事务数量
     */
    private void rollbackPartially(MultiTransactionContext context, int successCount) {
        if (context == null || successCount <= 0) {
            return;
        }
        List<Map.Entry<DataSourceKey, MultiTransactionContext.TransactionEntry>> entries = new ArrayList<>(context.getTransactionMap().entrySet());

        for (int i = successCount - 1; i >= 0; i--) {
            Map.Entry<DataSourceKey, MultiTransactionContext.TransactionEntry> entry = entries.get(i);
            try {
                entry.getValue().getTxManager().rollback(entry.getValue().getTxStatus());
                log.debug("部分开启回滚成功: datasource={}", entry.getKey().getType());
            } catch (Exception e) {
                log.error("部分开启回滚失败: datasource={}", entry.getKey().getType(), e);
            }
        }
    }
}

MultiDataSourceTxManagerRegistry 多数据源事务管理器注册中心

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;

import java.util.EnumMap;
import java.util.Map;

/**
 * 多数据源事务管理器注册中心
 *
 * <p>管理每个数据源对应的独立 {@link PlatformTransactionManager} 实例,
 * 供 {@code MultiTransactionManager} 开启各数据源的独立事务。
 *
 * <p>设计模式:注册表模式(Registry Pattern),集中管理所有数据源事务管理器。
 *
 * <p>扩展指南:
 * <ol>
 *   <li>在 {@link DataSourceProxyConfig} 中创建新的数据源事务管理器 Bean</li>
 *   <li>在本类构造函数中添加对应的注入参数</li>
 *   <li>在 {@link DataSourceKey} 枚举中确保有对应项</li>
 * </ol>
 *
 * @author team
 * @since 1.0
 */
@Slf4j
@Component
public class MultiDataSourceTxManagerRegistry {

    /**
     * 数据源类型 → 事务管理器映射
     *
     * <p>使用 EnumMap 因为键是枚举类型,性能优于 HashMap
     */
    private final Map<DataSourceKey, PlatformTransactionManager> registry;

    /**
     * 构造注册中心,注入所有数据源对应的独立事务管理器
     *
     * <p>注意:每个数据源的事务管理器都是独立创建的,绑定到各自的物理 DataSource,
     * 与 DataSourceProxyConfig 中的 DynamicDataSource 事务管理器互不影响。
     */
    public MultiDataSourceTxManagerRegistry(@Qualifier("orderTransactionManager") PlatformTransactionManager orderTxManager, @Qualifier("storageTransactionManager") PlatformTransactionManager storageTxManager, @Qualifier("payTransactionManager") PlatformTransactionManager payTxManager) {
        this.registry = new EnumMap<>(DataSourceKey.class);
        registry.put(DataSourceKey.ORDER, orderTxManager);
        registry.put(DataSourceKey.STORAGE, storageTxManager);
        registry.put(DataSourceKey.PAY, payTxManager);
        log.info("多数据源事务管理器注册中心初始化完成,已注册 {} 个数据源的事务管理器", registry.size());
    }

    /**
     * 根据数据源类型获取对应的事务管理器
     *
     * @param DataSourceKey 数据源类型枚举
     * @return 该数据源的事务管理器
     * @throws IllegalArgumentException 如果指定数据源类型未注册
     */
    public PlatformTransactionManager getTransactionManager(DataSourceKey DataSourceKey) {
        PlatformTransactionManager txManager = registry.get(DataSourceKey);
        if (txManager == null) {
            throw new IllegalArgumentException(String.format("数据源 [%s] 的事务管理器未注册,请检查配置", DataSourceKey.getType()));
        }
        return txManager;
    }

    /**
     * 检查指定数据源是否已注册
     *
     * @param DataSourceKey 数据源类型
     * @return true 表示已注册
     */
    public boolean contains(DataSourceKey DataSourceKey) {
        return registry.containsKey(DataSourceKey);
    }
}

开启分布式事物

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 跨库事务 AOP 切面
 *
 * <p>拦截标注了 {@link MultiTransactional} 注解的方法,自动管理
 * 跨多个数据库的事务开启、提交和回滚。
 *
 * <p>执行流程:
 * <ol>
 *   <li>解析 {@code @MultiTransactional} 注解获取参与事务的数据源列表</li>
 *   <li>调用 {@link MultiTransactionManager#begin(DataSourceKey...)} 开启所有事务</li>
 *   <li>执行目标方法(业务逻辑通过 {DynamicDataSourceContextHolder.setDataSourceType()} 切换数据源)</li>
 *   <li>方法正常返回 → 提交所有事务</li>
 *   <li>方法抛出异常 → 回滚所有事务</li>
 *   <li>清理 ThreadLocal 上下文</li>
 * </ol>
 *
 * <p>优先级说明:{@code @Order(0)} 确保本切面在 {@code DataSourceSwitchAspect}(Order=-10000)
 * 的 {@code @Before} 之后执行,保证数据源上下文已初始化后再开启事务。
 *
 * @author team
 * @since 1.0
 */
@Slf4j
@Aspect
@Component
@Order(0)
public class MultiTransactionalAspect {

    private final MultiTransactionManager multiTransactionManager;

    public MultiTransactionalAspect(MultiTransactionManager multiTransactionManager) {
        this.multiTransactionManager = multiTransactionManager;
    }

    /**
     * 拦截 @MultiTransactional 注解的方法
     *
     * <p>使用 {@code @annotation} 切点表达式匹配方法上的注解,
     * 同时支持类级别和方法级别的注解(方法级别优先)。
     *
     * @param joinPoint 切点,用于执行目标方法
     * @return 目标方法的返回值
     * @throws Throwable 目标方法抛出的异常
     */
    @Around("@annotation(net.test.project.annotation.MultiTransactional)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method interfaceMethod = signature.getMethod();

        /*
         * 使用 AopUtils.getMostSpecificMethod() 获取目标实现类上的具体方法,
         * 兼容 JDK 动态代理(接口方法)和 CGLIB(实现类方法)两种代理模式。
         * 直接使用 signature.getMethod() 在 JDK 代理下返回接口方法,
         * 接口方法上没有 @MultiTransactional 注解,会导致注解查找失败、事务静默失效。
         */
        Method method = AopUtils.getMostSpecificMethod(interfaceMethod, joinPoint.getTarget().getClass());

        MultiTransactional annotation = method.getAnnotation(MultiTransactional.class);
        if (annotation == null) {
            /*
             * 尝试从类级别获取注解
             */
            annotation = joinPoint.getTarget().getClass().getAnnotation(MultiTransactional.class);
        }
        if (annotation == null) {
            log.warn("MultiTransactionalAspect 未找到注解,直接执行方法: {}", method.getName());
            return joinPoint.proceed();
        }

        DataSourceKey[] datasources = annotation.value();
        if (datasources == null || datasources.length == 0) {
            log.warn("@MultiTransactional 未指定数据源,直接执行方法: {}", method.getName());
            return joinPoint.proceed();
        }

        String methodName = joinPoint.getTarget().getClass().getSimpleName() + "." + method.getName();
        log.info("跨库事务开始: method={}, datasources={}", methodName, datasources);

        MultiTransactionContext context = null;
        try {
            /*
             * 阶段1: 为所有声明的数据源开启独立事务
             * MultiTransactionManager.begin() 内部会切换 DynamicDataSourceContextHolder 为
             * 每个数据源开启事务,确保 DataSourceTransactionManager 从正确的
             * 物理数据源获取连接
             */
            context = multiTransactionManager.begin(datasources);

            /*
             * 阶段2: 执行业务方法
             * 业务方法内部通过 DynamicDataSourceContextHolder.setDataSourceType() 切换 MyBatis 路由,
             * Spring 的 TransactionSynchronizationManager 会自动将各数据源的
             * DB 操作绑定到对应的事务连接上
             */
            Object result = joinPoint.proceed();

            /*
             * 阶段3: 业务成功,提交所有事务
             */
            multiTransactionManager.commit(context);
            log.info("跨库事务提交成功: method={}", methodName);

            return result;
        } catch (Throwable e) {
            /*
             * 阶段4: 业务失败,回滚所有事务
             */
            log.error("跨库事务回滚: method={}, 异常信息={}", methodName, e.getMessage());
            if (context != null) {
                multiTransactionManager.rollback(context);
            }
            throw e;
        } finally {
            /*
             * 阶段5: 清理 ThreadLocal,防止内存泄漏
             */
            multiTransactionManager.cleanup(context);
        }
    }
}

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐