基于springboot的多数据源管理

背景

开发过程中,一个应用中长面林多个数据源场景。springboot提供的默认数据源装配只针对单数据源场景。本文基于springboot+mysql+mybatis+aop实现多数据源切换

实现重点

AbstractRoutingDataSource

spring本省提供的多数据源管理类。

主要关注

setTargetDataSources,设置多数据源,将数据源统一管理,底层是一个Map结构,key数据源key,value为数据源配置dataSource

determineCurrentLookupKey,从多数据源挑选需要切换的数据源

DataSourceRoutingDataSource

继承AbstractRoutingDataSource,实现determineCurrentLookupKey方法,根据业务挑选对应数据源

public class DataSourceRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        final DataSourceKey dataSourceKey = DynamicDataSourceContextHolder.getDataSourceKey();
        log.info("Current DataSource is [{}]", dataSourceKey);
        return dataSourceKey;
    }
}

 

DynamicDataSourceContextHolder

数据源key上下文持有者,使用ThreadLocal存储

public class DynamicDataSourceContextHolder {

    /**
     * 用于在切换数据源时保证不会被其他线程修改
     */
    private static Lock lock = new ReentrantLock();

    /**
     * 用于轮循的计数器
     */
    private static int counter = 0;

    private static final ThreadLocal<Object> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * All DataSource List
     */
    public static List<Object> dataSourceKeys = new ArrayList<>();

    /**
     * The constant slaveDataSourceKeys.
     */
    public static List<Object> slaveDataSourceKeys = new ArrayList<>();

    public static void setDataSourceKey(DataSourceKey key) {
        CONTEXT_HOLDER.set(key);
    }

    public static void useMobileDataSource() {
        CONTEXT_HOLDER.set(DataSourceKey.DS0);
    }


    public static DataSourceKey getDataSourceKey() {
        DataSourceKey key = (DataSourceKey) CONTEXT_HOLDER.get();
        return key==null?DataSourceKey.DS0:key;
    }

    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }
}

 

DataSourceConfigurer

多数据源配置类,用于装载配置文件中配置的多数据源,比如放置在application.yml

spring:
  datasource:
    druid:
      ds0:
        name: ds0
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:13306/ds0?serverTimezone=UTC&characterEncoding=utf-8&useSSL=false
        type: com.alibaba.druid.pool.DruidDataSource
      ds1:
        name: ds1
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:13306/ds1?serverTimezone=UTC&characterEncoding=utf-8&useSSL=false
        type: com.alibaba.druid.pool.DruidDataSource
      ds2:
        name: ds2
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:13306/ds2?serverTimezone=UTC&characterEncoding=utf-8&useSSL=false
        type: com.alibaba.druid.pool.DruidDataSource

# mybatis 配置
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations:
    - classpath:mybatis/transaction.xml

 

具体实现如下


@Configuration
@EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE) // 确保事务切面最后执行
public class DataSourceConfigurer {

/**
* @Primary 注解用于标识默认使用的 DataSource Bean,因为有多个 DataSource Bean,该注解可用于 master
* 或 slave DataSource Bean, 但不能用于 dynamicDataSource Bean, 否则会产生循环调用
* @ConfigurationProperties 注解用于从 application.properties 文件中读取配置,为 Bean 设置属性
* @return data source
*/
@Primary
@Bean("ds0")
@ConfigurationProperties("spring.datasource.druid.ds0")
public DataSource ds0(){
return DruidDataSourceBuilder.create().build();
}

@Bean("ds1")
@ConfigurationProperties("spring.datasource.druid.ds1")
public DataSource ds1(){
return DruidDataSourceBuilder.create().build();
}

@Bean("ds2")
@ConfigurationProperties("spring.datasource.druid.ds2")
public DataSource ds2(){
return DruidDataSourceBuilder.create().build();
}


@Bean("dynamicDataSource")
public DataSource dynamicDataSource(@Qualifier(value = "ds0") DataSource ds0,
@Qualifier(value = "ds1") DataSource ds1,
@Qualifier(value = "ds2") DataSource ds2){
DataSourceRoutingDataSource dataSourceRoutingDataSource = new DataSourceRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(10);
dataSourceMap.put(DataSourceKey.DS0,ds0);
dataSourceMap.put(DataSourceKey.DS1,ds1);
dataSourceMap.put(DataSourceKey.DS2,ds2);
// 默认指定的数据源
dataSourceRoutingDataSource.setDefaultTargetDataSource(ds0);
// 将 order 和 其它 数据源作为指定的数据源
dataSourceRoutingDataSource.setTargetDataSources(dataSourceMap);
return dataSourceRoutingDataSource;
}

/**
* 配置 SqlSessionFactoryBean
* @ConfigurationProperties 在这里是为了将 MyBatis 的 mapper 位置和持久层接口的别名设置到
* Bean 的属性中,如果没有使用 *.xml 则可以不用该配置,否则将会产生 invalid bond statement 异常
*
* @return the sql session factory bean
*/
@Bean
@ConfigurationProperties(prefix = "mybatis")
@ConditionalOnBean(name = "dynamicDataSource")
public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource 作为数据源则不能实现切换
sqlSessionFactoryBean.setDataSource(dynamicDataSource);
return sqlSessionFactoryBean;
}


/**
* 注入 DataSourceTransactionManager 用于事务管理
*/
@Bean
@ConditionalOnBean(name = "dynamicDataSource")
public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dynamicDataSource);
transactionManager.setTransactionSynchronization(DataSourceTransactionManager.SYNCHRONIZATION_ALWAYS);
return transactionManager;
}
}

  

RoutingWith

多数据源切换注解,可放置在方法或者类上

@Target({ElementType.TYPE,ElementType.METHOD,ElementType.ANNOTATION_TYPE,ElementType.FIELD,ElementType.PACKAGE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RoutingWith {
    DataSourceKey routerKey() default DataSourceKey.DS0;
}
DataSourceKey 为多数据源key,后续使用切面拦截,获取当前注解中的DataSourceKey,设置到holder上下文,在DataSourceRoutingDataSource中进行数据源选择时从上下文holder中获取,完成数据源切换

DynamicDataSourceAspect

数据源注解切面,完成注解key获取并设置,为后续数据源切换提供key支持

需要注意点为:

多数据源在面对事物时不做特殊处理会导致切换失败,其原如下

1、spring事务管理在方法开始时确定数据源,后续不在切换

2、事务开启后持有当前数据源连接,导致无法切换

3、事务AOP和数据源切换AOP的执行顺序可能不正确,需要保证数据源切换AOP在事务之前执行


@Slf4j
@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 1) // 确保在事务切面前执行
public class DynamicDataSourceAspect {

/**
* 针对类级别拦截
* @param joinPoint
* @param routingWith
* @return
* @throws Throwable
*/
@Before("@within(routingWith)")
public void routingWithDataSourceBefore(JoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
doBefore(routingWith);
}

@Before("@within(routingWith)")
public void routingWithDataSourceAfter(JoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
doAfter();
}


/**
* 针对方法级别拦截
* @param joinPoint
* @param routingWith
* @return
* @throws Throwable
*/
@Before("@annotation(routingWith)")
public void routingWithDataSourceMBefore(JoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
doBefore(routingWith);
}


@After("@annotation(routingWith)")
public void routingWithDataSourceMAfter(JoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
doBefore(routingWith);
}



private void doBefore(RoutingWith routingWith){
DataSourceKey key = routingWith.routerKey();
if (DynamicDataSourceContextHolder.getDataSourceKey()!=key){
// 设置当前数据源
DynamicDataSourceContextHolder.setDataSourceKey(key);
// 注册事务同步回调
if (TransactionSynchronizationManager.isActualTransactionActive()){
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCompletion(int status) {
DynamicDataSourceContextHolder.clearDataSourceKey();
}
});
}
}
}

private void doAfter() {
if (!TransactionSynchronizationManager.isActualTransactionActive()) {
DynamicDataSourceContextHolder.clearDataSourceKey();
}
}

  

 

posted @ 2025-07-21 22:05  多少幅度  阅读(19)  评论(0)    收藏  举报