极简的多数据源动态切换配置
By: Date: 2021年2月4日 Categories: 程序 标签:

项目上常常需要多个数据源来存取数据,最近项目上试用了Tidb,因为兼容MySql协议,我们可以直接像Mysql一样在项目中进行配置。那么如何快速的将单一的数据源改成多数据源,并能够在项目中动态的切换就是本次要说的事情。下面精简并省略了无关代码,主要使用`AbstractRoutingDataSource`实现,使用AOP切换,在此记录配置过程以便查阅,分分钟解决问题。

yml配置文件中数据源配置:

spring:
  datasource:
    druid:
      localdb:
        url: jdbc:mysql://127.0.0.1:3306/story_web?useUnicode=true&characterEncoding=utf-8&useSSL=false
        username: root
		password: ******
        driver-class-name: com.mysql.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource
      tidb:
        url: jdbc:mysql://127.0.0.1:3306/story_log?useUnicode=true&characterEncoding=utf-8&useSSL=false
        username: root
		password: ******
        driver-class-name: com.mysql.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource

定义枚举,将我们有的数据库都定义在其中:

public enum DBTypeEnum {
    localdb("localdb"),
    tidb("tidb");
    private String value;

    DBTypeEnum(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

数据库上下文,用于记录当前上下文所使用的数据源是谁:

public class DbContextHolder {
    private static final ThreadLocal contextHolder = new ThreadLocal<>();
    /**
     * 设置数据源
     * @param dbTypeEnum
     */
    public static void setDbType(DBTypeEnum dbTypeEnum) {
        contextHolder.set(dbTypeEnum.getValue());
    }

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

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

通过AbstractRoutingDataSource抽象类可以动态的切换数据源

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        String dbType = DbContextHolder.getDbType();
        DbContextHolder.clearDbType();
        return dbType;
    }
}

数据源配置,创建所有的数据源,添加到数据源路由中,并设置默认数据源:

@EnableTransactionManagement
@Configuration
public class MybatisPlusDsConfig {

    @Bean(name = "localdb")
    @ConfigurationProperties(prefix = "spring.datasource.druid.localdb")
    public DataSource db1() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "tidb")
    @ConfigurationProperties(prefix = "spring.datasource.druid.tidb")
    public DataSource db2() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 动态数据源配置
     * @return
     */
    @Bean
    @Primary
    public DataSource multipleDataSource(@Qualifier("localdb") DataSource localdb, @Qualifier("tidb") DataSource tidb) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DBTypeEnum.localdb.getValue(), localdb);
        targetDataSources.put(DBTypeEnum.tidb.getValue(), tidb);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(localdb);
        return dynamicDataSource;
    }
}

最后一步,通过AOP来切换数据源:

@Slf4j
@Order(1)
@Aspect
@Component
public class DataSourceSwitchAspect {
    @Pointcut("execution(* com.story.storyweb.mapper.localmapper..*.*(..))")
    private void localMpperAspect() {
    }

    @Pointcut("execution(* com.story.storyweb.mapper.timapper..*.*(..))")
    private void tiMpperAspect() {
    }

    @Before("localMpperAspect()")
    public void db1() {
        log.info("切换到localdb数据源...");
        DbContextHolder.setDbType(DBTypeEnum.localdb);
    }

    @Before("tiMpperAspect()")
    public void db2() {
        log.info("切换到tidb数据源...");
        DbContextHolder.setDbType(DBTypeEnum.tidb);
    }
}

因为我们使用了mybatis-plus,这里定义两个切点在不同的mapper目录上,可以看到:

  1. com.story.storyweb.mapper.localmapper目录下的mapper会使用localdb数据源。
  2. com.story.storyweb.mapper.timapper目录下的mapper会切换到tidb数据源。

当然,如果我们的项目不需要事务,那这里的切点放在mapper上并不会有问题。
但当开启了数据库事务,而事务一般会在service上开始,那么,切点放在mapper上,在切换数据源就会遇到错误哦。
这里只为了演示将切点放在了mapper上,实际上,你可以放在你想切换的任何位置。

接下来,工程里的mapper当然按照下面的结构来区分:
/mapper/localmapper
/mapper/timapper

至此,打完收工。

发表评论

邮箱地址不会被公开。 必填项已用*标注