专栏名称: 架构师
架构师云集,三高架构(高可用、高性能、高稳定)、大数据、机器学习、Java架构、系统架构、大规模分布式架构、人工智能等的架构讨论交流,以及结合互联网技术的架构调整,大规模架构实战分享。欢迎有想法、乐于分享的架构师交流学习。
目录
51好读  ›  专栏  ›  架构师

Spring Boot 如何优雅整合多数据源

架构师  · 公众号  ·  · 2025-03-12 22:28

正文


架构师(JiaGouX)
我们都是架构师!
架构未来,你来不来?




有读者问到如何自定义实现 Spring Boot 多数据源,这里分享一篇 基于 ThreadLocal AbstractRoutingDataSource 的动态数据源切换方案,非常详细。

下面是正文。

最近在做业务需求时,需要从不同的数据库中获取数据然后写入到当前数据库中,因此涉及到切换数据源问题。本来想着使用 Mybatis-plus 中提供的动态数据源 SpringBoot 的 starter:dynamic-datasource-spring-boot-starter (地址:
https://github.com/baomidou/dynamic-datasource )来实现。结果引入后发现由于之前项目环境问题导致无法使用。然后研究了下数据源切换代码,决定自己采用 ThreadLocal + AbstractRoutingDataSource 来模拟实现 dynamic-datasource-spring-boot-starter 中线程数据源切换。


1、简介


上述提到了 ThreadLocal AbstractRoutingDataSource ,我们来对其进行简单介绍下:

  • ThreadLocal :想必大家必不会陌生,全称:thread local variable。主要是为解决多线程时由于并发而产生数据不一致问题。ThreadLocal 为每个线程提供变量副本,确保每个线程在某一时间访问到的不是同一个对象,这样做到了隔离性,增加了内存,但大大减少了线程同步时的性能消耗,减少了线程并发控制的复杂程度。 ThreadLocal 原理: ThreadLocal 存入值时,会获取当前线程实例作为 key,存入当前线程对象中的 Map 中。
  • AbstractRoutingDataSource :根据用户定义的规则选择当前的数据源。在执行查询之前,设置使用的数据源,实现动态路由的数据源,在每次数据库查询操作前执行它的抽象方法 determineCurrentLookupKey() ,决定使用哪个数据源。

2、代码实现


技术栈:
SpringBoot2.4.8 + Mybatis-plus3.2.0 + Druid1.2.6 + Lombok1.18.20 + commons-lang3 3.10

2.1 实现 ThreadLocal

创建一个类用于实现 ThreadLocal ,主要是通过 get set remove 方法来获取、设置、删除当前线程对应的数据源。

/**
 * @author : jiangjs
 * @description:
 * @date: 2023/7/27 11:21
 **/

public class DataSourceContextHolder {
    //此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。
    private static final ThreadLocal DATASOURCE_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源
     * @param dataSourceName 数据源名称
     */

    public static void setDataSource(String dataSourceName){
        DATASOURCE_HOLDER.set(dataSourceName);
    }

    /**
     * 获取当前线程的数据源
     * @return 数据源名称
     */

    public static String getDataSource(){
        return DATASOURCE_HOLDER.get();
    }

    /**
     * 删除当前数据源
     */

    public static void removeDataSource(){
        DATASOURCE_HOLDER.remove();
    }

}

2.2 实现 AbstractRoutingDataSource

定义一个动态数据源类实现 AbstractRoutingDataSource ,通过 determineCurrentLookupKey 方法与上述实现的 ThreadLocal 类中的 get 方法进行关联,实现动态切换数据源。

/**
 * @author: jiangjs
 * @description: 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
 * @date: 2023/7/27 11:18
 **/

public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultDataSource,Map targetDataSources){
        super.setDefaultTargetDataSource(defaultDataSource);
        super.setTargetDataSources(targetDataSources);
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

上述代码中,还实现了一个动态数据源类的构造方法,主要是为了设置默认数据源,以及以 Map 保存的各种目标数据源。其中 Map 的 key 是设置的数据源名称,value 则是对应的数据源(DataSource)。

2.3 配置数据库

application.yml 中配置数据库信息:

#设置数据源
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master:
        url: jdbc:mysql://xxxxxx:3306/test1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave:
        url: jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      initial-size: 15
      min-idle: 15
      max-active: 200
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: ""
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: false
      connection-properties: false

数据源配置:

/**
 * @author: jiangjs
 * @description: 设置数据源
 * @date: 2023/7/27 11:34
 **/

@Configuration
public class DateSourceConfig {

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

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

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource createDynamicDataSource(){
        Map dataSourceMap = new HashMap<>();
        DataSource defaultDataSource = masterDataSource();
        dataSourceMap.put("master",defaultDataSource);
        dataSourceMap.put("slave",slaveDataSource());
        return new DynamicDataSource(defaultDataSource,dataSourceMap);
    }

}

通过配置类,将配置文件中的配置的数据库信息转换成 datasource,并添加到 DynamicDataSource 中,同时通过 @Bean DynamicDataSource 注入 Spring 中进行管理,后期在进行动态数据源添加时,会用到。

2.4 测试

在主从两个测试库中,分别添加一张表 test_user ,里面只有一个字段 user_name

create table test_user(
user_name varchar(255) not null comment '用户名'
)

在主库添加信息:

insert into test_user (user_name) value ('master');

从库中添加信息:

insert into test_user (user_name) value ('slave');

我们创建一个 getData 的方法,参数就是需要查询数据的数据源名称。

@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName){
    DataSourceContextHolder.setDataSource(datasourceName);
    TestUser testUser = testUserMapper.selectOne(null);
    DataSourceContextHolder.removeDataSource();
    return testUser.getUserName();
}

其他的 Mapper 和实体类大家自行实现。

执行结果:

1、传递 master 时:

2、传递 slave 时:

通过执行结果,我们看到传递不同的数据源名称,查询对应的数据库是不一样的,返回结果也不一样。

在上述代码中,我们看到 DataSourceContextHolder.setDataSource(datasourceName); 来设置了当前线程需要查询的数据库,通过 DataSourceContextHolder.removeDataSource(); 来移除当前线程已设置的数据源。使用过 Mybatis-plus 动态数据源的小伙伴,应该还记得我们在使用切换数据源时会使用到 DynamicDataSourceContextHolder.push(String ds); DynamicDataSourceContextHolder.poll(); 这两个方法,翻看源码我们会发现其实就是在使用 ThreadLocal 时使用了栈,这样的好处就是能使用多数据源嵌套,这里就不带大家实现了,有兴趣的小伙伴可以看看 Mybatis-plus 中动态数据源的源码。

注:启动程序时,小伙伴不要忘记将 SpringBoot 自动添加数据源进行排除哦,否则会报循环依赖问题。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

2.5 优化调整

2.5.1 注解切换数据源

在上述中,虽然已经实现了动态切换数据源,但是我们会发现如果涉及到多个业务进行切换数据源的话,我们就需要在每一个实现类中添加这一段代码。

说到这有小伙伴应该就会想到使用注解来进行优化,接下来我们来实现一下。

1、定义注解

我们就用 mybatis 动态数据源切换的注解: @DS

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/7/27 14:39
 **/

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
    String value() default "master";
}

2、实现 AOP

@Aspect
@Component
@Slf4j
public class DSAspect {

    @Pointcut("@annotation(com.jiashn.dynamic_datasource.dynamic.aop.DS)")
    public void dynamicDataSource(){}

    @Around("dynamicDataSource()")
    public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature)point.getSignature();
        Method method = signature.getMethod();
        DS ds = method.getAnnotation(DS.class);
        if (Objects.nonNull(ds)){
            DataSourceContextHolder.setDataSource(ds.value());
        }
        try {
            return point.proceed();
        } finally {
            DataSourceContextHolder.removeDataSource();
        }
    }
}

代码使用了@Around,通过 ProceedingJoinPoint 获取注解信息,拿到注解传递值,然后设置当前线程的数据源。

3、测试

添加两个测试方法:

@GetMapping("/getMasterData.do")
public String getMasterData(){
    TestUser testUser = testUserMapper.selectOne(null);
    return testUser.getUserName();
}

@GetMapping("/getSlaveData.do")
@DS("slave")
public String getSlaveData(){
    TestUser testUser = testUserMapper.selectOne(null);
    return testUser.getUserName();
}

由于 @DS 中设置的默认值是:master,因此在调用主数据源时,可以不用进行添加。

执行结果:

1、调用 getMasterData.do 方法:







请到「今天看啥」查看全文