SpringBoot 实现动态切换数据源,这样做才更优雅!


最近在做业务需求时,需要从不同的数据库中获取数据然后写入到当前数据库中,因此涉及到切换数据源问题。本来想着使用 Mybatis-plus 中提供的动态数据源 SpringBoot 的 starter:dynamic-datasource-spring-boot-starter 来实现。
结果引入后发现由于之前项目环境问题导致无法使用。然后研究了下数据源切换代码,决定自己采用 ThreadLocal+AbstractRoutingDataSource 来模拟实现 dynamic-datasource-spring-boot-starter 中线程数据源切换。
1 简介
上述提到了 ThreadLocal 和 AbstractRoutingDataSource,我们来对其进行简单介绍下。
ThreadLocal:想必大家必不会陌生,全称:thread local variable。主要是为解决多线程时由于并发而产生数据不一致问题。ThreadLocal 为每个线程提供变量副本,确保每个线程在某一时间访问到的不是同一个对象,这样做到了隔离性,增加了内存,但大大减少了线程同步时的性能消耗,减少了线程并发控制的复杂程度。
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 方法来获取、设置、删除当前线程对应的数据源。

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

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

通过配置类,将配置文件中的配置的数据库信息转换成 datasource,并添加到 DynamicDataSource 中,同时通过 @Bean 将 DynamicDataSource 注入 Spring 中进行管理,后期在进行动态数据源添加时,会用到。
2.4 测试
在主从两个测试库中,分别添加一张表 test_user,里面只有一个字段 user_name。
在主库添加信息:insert into test_user (user_name) value ('master');
从库中添加信息:insert into test_user (user_name) value ('slave');
我们创建一个 getData 的方法,参数就是需要查询数据的数据源名称。

其他的 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 注解切换数据源
在上述中,虽然已经实现了动态切换数据源,但是我们会发现如果涉及到多个业务进行切换数据源的话,我们就需要在每一个实现类中添加这一段代码。
说到这有小伙伴应该就会想到使用注解来进行优化,接下来我们来实现一下。
2.5.1.1 定义注解
我们就用 mybatis 动态数据源切换的注解:DS,代码如下:

2.5.1.2 实现 aop

代码使用了 @Around,通过 ProceedingJoinPoint 获取注解信息,拿到注解传递值,然后设置当前线程的数据源。对 aop 不了解的小伙伴可以自行 google 或百度。
2.5.1.3 测试
添加两个测试方法:

由于 @DS 中设置的默认值是:master,因此在调用主数据源时,可以不用进行添加。
执行结果:
1、调用 getMasterData.do 方法:

2、调用 getSlaveData.do 方法:

通过执行结果,我们通过 @DS 也进行了数据源的切换,实现了 Mybatis-plus 动态切换数据源中的通过注解切换数据源的方式。
2.5.2 动态添加数据源
业务场景 :有时候我们的业务会要求我们从保存有其他数据源的数据库表中添加这些数据源,然后再根据不同的情况切换这些数据源。
因此我们需要改造下 DynamicDataSource 来实现动态加载数据源。
2.5.2.1 数据源实体

实体中定义数据源的一般信息,同时定义一个 key 用于作为 DynamicDataSource 中 Map 中的 key。
2.5.2.2 修改 DynamicDataSource

在改造后的 DynamicDataSource 中,我们添加可以一个 private final Map<Object,Object> targetDataSourceMap,这个 map 会在添加数据源的配置文件时将创建的 Map 数据源信息通过 DynamicDataSource 构造方法进行初始赋值,即:DateSourceConfig 类中的 createDynamicDataSource()方法中。
同时我们在该类中添加了一个 createDataSource 方法,进行数据源的创建,并添加到 map 中,再通过 super.setTargetDataSources(this.targetDataSourceMap) ;进行目标数据源的重新赋值。
2.5.2.3 动态添加数据源
上述代码已经实现了添加数据源的方法,那么我们来模拟通过从数据库表中添加数据源,然后我们通过调用加载数据源的方法将数据源添加进数据源 Map 中。
在主数据库中定义一个数据库表,用于保存数据库信息。
为了方便,我们将之前的从库录入到数据库中,修改数据库名称。
数据库表对应的实体、mapper,小伙伴们自行添加。
启动 SpringBoot 时添加数据源:

经过上述 SpringBoot 启动后,已经将数据库表中的数据添加到动态数据源中,我们调用之前的测试方法,将数据源名称作为参数传入看看执行结果。
2.5.2.4 测试

通过测试我们发现数据库表中的数据库被动态加入了数据源中,小伙伴可以愉快地随意添加数据源了。
赠人玫瑰,手留余香~ LZ整理了一整套2023年最新的面试资料,如果有需要的小伙伴点击此处即可~
转载
作者:JAVA
评论