SpringBoot實現(xiàn)動態(tài)切換數(shù)據(jù)源的示例代碼
最近在做業(yè)務(wù)需求時,需要從不同的數(shù)據(jù)庫中獲取數(shù)據(jù)然后寫入到當(dāng)前數(shù)據(jù)庫中,因此涉及到切換數(shù)據(jù)源問題。本來想著使用Mybatis-plus中提供的動態(tài)數(shù)據(jù)源SpringBoot的starter:dynamic-datasource-spring-boot-starter來實現(xiàn)。
結(jié)果引入后發(fā)現(xiàn)由于之前項目環(huán)境問題導(dǎo)致無法使用。然后研究了下數(shù)據(jù)源切換代碼,決定自己采用ThreadLocal+AbstractRoutingDataSource來模擬實現(xiàn)dynamic-datasource-spring-boot-starter中線程數(shù)據(jù)源切換。
1 簡介
上述提到了ThreadLocal和AbstractRoutingDataSource,我們來對其進(jìn)行簡單介紹下。
ThreadLocal:想必大家必不會陌生,全稱:thread local variable。主要是為解決多線程時由于并發(fā)而產(chǎn)生數(shù)據(jù)不一致問題。ThreadLocal為每個線程提供變量副本,確保每個線程在某一時間訪問到的不是同一個對象,這樣做到了隔離性,增加了內(nèi)存,但大大減少了線程同步時的性能消耗,減少了線程并發(fā)控制的復(fù)雜程度。
- ThreadLocal作用:在一個線程中共享,不同線程間隔離
- ThreadLocal原理:ThreadLocal存入值時,會獲取當(dāng)前線程實例作為key,存入當(dāng)前線程對象中的Map中。
AbstractRoutingDataSource:根據(jù)用戶定義的規(guī)則選擇當(dāng)前的數(shù)據(jù)源,
作用:在執(zhí)行查詢之前,設(shè)置使用的數(shù)據(jù)源,實現(xiàn)動態(tài)路由的數(shù)據(jù)源,在每次數(shù)據(jù)庫查詢操作前執(zhí)行它的抽象方法determineCurrentLookupKey(),決定使用哪個數(shù)據(jù)源。
2 代碼實現(xiàn)
程序環(huán)境:
- SpringBoot2.4.8
- Mybatis-plus3.2.0
- Druid1.2.6
- lombok1.18.20
- commons-lang3 3.10
2.1 實現(xiàn)ThreadLocal
創(chuàng)建一個類用于實現(xiàn)ThreadLocal,主要是通過get,set,remove方法來獲取、設(shè)置、刪除當(dāng)前線程對應(yīng)的數(shù)據(jù)源。
/**
* @author: jiangjs
* @description:
* @date: 2023/7/27 11:21
**/
public class DataSourceContextHolder {
//此類提供線程局部變量。這些變量不同于它們的正常對應(yīng)關(guān)系是每個線程訪問一個線程(通過get、set方法),有自己的獨(dú)立初始化變量的副本。
private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
/**
* 設(shè)置數(shù)據(jù)源
* @param dataSourceName 數(shù)據(jù)源名稱
*/
public static void setDataSource(String dataSourceName){
DATASOURCE_HOLDER.set(dataSourceName);
}
/**
* 獲取當(dāng)前線程的數(shù)據(jù)源
* @return 數(shù)據(jù)源名稱
*/
public static String getDataSource(){
return DATASOURCE_HOLDER.get();
}
/**
* 刪除當(dāng)前數(shù)據(jù)源
*/
public static void removeDataSource(){
DATASOURCE_HOLDER.remove();
}
}
2.2 實現(xiàn)AbstractRoutingDataSource
定義一個動態(tài)數(shù)據(jù)源類實現(xiàn)AbstractRoutingDataSource,通過determineCurrentLookupKey方法與上述實現(xiàn)的ThreadLocal類中的get方法進(jìn)行關(guān)聯(lián),實現(xiàn)動態(tài)切換數(shù)據(jù)源。
/**
* @author: jiangjs
* @description: 實現(xiàn)動態(tài)數(shù)據(jù)源,根據(jù)AbstractRoutingDataSource路由到不同數(shù)據(jù)源中
* @date: 2023/7/27 11:18
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
上述代碼中,還實現(xiàn)了一個動態(tài)數(shù)據(jù)源類的構(gòu)造方法,主要是為了設(shè)置默認(rèn)數(shù)據(jù)源,以及以Map保存的各種目標(biāo)數(shù)據(jù)源。其中Map的key是設(shè)置的數(shù)據(jù)源名稱,value則是對應(yīng)的數(shù)據(jù)源(DataSource)。
2.3 配置數(shù)據(jù)庫
application.yml中配置數(shù)據(jù)庫信息:
#設(shè)置數(shù)據(jù)源
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: 設(shè)置數(shù)據(jù)源
* @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<Object,Object> dataSourceMap = new HashMap<>();
DataSource defaultDataSource = masterDataSource();
dataSourceMap.put("master",defaultDataSource);
dataSourceMap.put("slave",slaveDataSource());
return new DynamicDataSource(defaultDataSource,dataSourceMap);
}
}
通過配置類,將配置文件中的配置的數(shù)據(jù)庫信息轉(zhuǎn)換成datasource,并添加到DynamicDataSource中,同時通過@Bean將DynamicDataSource注入Spring中進(jìn)行管理,后期在進(jìn)行動態(tài)數(shù)據(jù)源添加時,會用到。
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');
我們創(chuàng)建一個getData的方法,參數(shù)就是需要查詢數(shù)據(jù)的數(shù)據(jù)源名稱。
@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和實體類大家自行實現(xiàn)。
執(zhí)行結(jié)果:
1、傳遞master時:

2、傳遞slave時:

通過執(zhí)行結(jié)果,我們看到傳遞不同的數(shù)據(jù)源名稱,查詢對應(yīng)的數(shù)據(jù)庫是不一樣的,返回結(jié)果也不一樣。
在上述代碼中,我們看到DataSourceContextHolder.setDataSource(datasourceName); 來設(shè)置了當(dāng)前線程需要查詢的數(shù)據(jù)庫,通過DataSourceContextHolder.removeDataSource(); 來移除當(dāng)前線程已設(shè)置的數(shù)據(jù)源。使用過Mybatis-plus動態(tài)數(shù)據(jù)源的小伙伴,應(yīng)該還記得我們在使用切換數(shù)據(jù)源時會使用到DynamicDataSourceContextHolder.push(String ds); 和DynamicDataSourceContextHolder.poll(); 這兩個方法,翻看源碼我們會發(fā)現(xiàn)其實就是在使用ThreadLocal時使用了棧,這樣的好處就是能使用多數(shù)據(jù)源嵌套,這里就不帶大家實現(xiàn)了,有興趣的小伙伴可以看看Mybatis-plus中動態(tài)數(shù)據(jù)源的源碼。
注:啟動程序時,小伙伴不要忘記將SpringBoot自動添加數(shù)據(jù)源進(jìn)行排除哦,否則會報循環(huán)依賴問題。
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
2.5 優(yōu)化調(diào)整
2.5.1 注解切換數(shù)據(jù)源
在上述中,雖然已經(jīng)實現(xiàn)了動態(tài)切換數(shù)據(jù)源,但是我們會發(fā)現(xiàn)如果涉及到多個業(yè)務(wù)進(jìn)行切換數(shù)據(jù)源的話,我們就需要在每一個實現(xiàn)類中添加這一段代碼。
說到這有小伙伴應(yīng)該就會想到使用注解來進(jìn)行優(yōu)化,接下來我們來實現(xiàn)一下。
2.5.1.1 定義注解
我們就用mybatis動態(tài)數(shù)據(jù)源切換的注解: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.5.1.2 實現(xiàn)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獲取注解信息,拿到注解傳遞值,然后設(shè)置當(dāng)前線程的數(shù)據(jù)源。對aop不了解的小伙伴可以自行g(shù)oogle或百度。
2.5.1.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中設(shè)置的默認(rèn)值是:master,因此在調(diào)用主數(shù)據(jù)源時,可以不用進(jìn)行添加。
執(zhí)行結(jié)果:
1、調(diào)用getMasterData.do方法:

2、調(diào)用getSlaveData.do方法:

通過執(zhí)行結(jié)果,我們通過@DS也進(jìn)行了數(shù)據(jù)源的切換,實現(xiàn)了Mybatis-plus動態(tài)切換數(shù)據(jù)源中的通過注解切換數(shù)據(jù)源的方式。
2.5.2 動態(tài)添加數(shù)據(jù)源
業(yè)務(wù)場景 :有時候我們的業(yè)務(wù)會要求我們從保存有其他數(shù)據(jù)源的數(shù)據(jù)庫表中添加這些數(shù)據(jù)源,然后再根據(jù)不同的情況切換這些數(shù)據(jù)源。
因此我們需要改造下DynamicDataSource來實現(xiàn)動態(tài)加載數(shù)據(jù)源。
2.5.2.1 數(shù)據(jù)源實體
/**
* @author: jiangjs
* @description: 數(shù)據(jù)源實體
* @date: 2023/7/27 15:55
**/
@Data
@Accessors(chain = true)
public class DataSourceEntity {
/**
* 數(shù)據(jù)庫地址
*/
private String url;
/**
* 數(shù)據(jù)庫用戶名
*/
private String userName;
/**
* 密碼
*/
private String passWord;
/**
* 數(shù)據(jù)庫驅(qū)動
*/
private String driverClassName;
/**
* 數(shù)據(jù)庫key,即保存Map中的key
*/
private String key;
}
實體中定義數(shù)據(jù)源的一般信息,同時定義一個key用于作為DynamicDataSource中Map中的key。
2.5.2.2 修改DynamicDataSource
/**
* @author: jiangjs
* @description: 實現(xiàn)動態(tài)數(shù)據(jù)源,根據(jù)AbstractRoutingDataSource路由到不同數(shù)據(jù)源中
* @date: 2023/7/27 11:18
**/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Map<Object,Object> targetDataSourceMap;
public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
this.targetDataSourceMap = targetDataSources;
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
/**
* 添加數(shù)據(jù)源信息
* @param dataSources 數(shù)據(jù)源實體集合
* @return 返回添加結(jié)果
*/
public void createDataSource(List<DataSourceEntity> dataSources){
try {
if (CollectionUtils.isNotEmpty(dataSources)){
for (DataSourceEntity ds : dataSources) {
//校驗數(shù)據(jù)庫是否可以連接
Class.forName(ds.getDriverClassName());
DriverManager.getConnection(ds.getUrl(),ds.getUserName(),ds.getPassWord());
//定義數(shù)據(jù)源
DruidDataSource dataSource = new DruidDataSource();
BeanUtils.copyProperties(ds,dataSource);
//申請連接時執(zhí)行validationQuery檢測連接是否有效,這里建議配置為TRUE,防止取到的連接不可用
dataSource.setTestOnBorrow(true);
//建議配置為true,不影響性能,并且保證安全性。
//申請連接的時候檢測,如果空閑時間大于timeBetweenEvictionRunsMillis,執(zhí)行validationQuery檢測連接是否有效。
dataSource.setTestWhileIdle(true);
//用來檢測連接是否有效的sql,要求是一個查詢語句。
dataSource.setValidationQuery("select 1 ");
dataSource.init();
this.targetDataSourceMap.put(ds.getKey(),dataSource);
}
super.setTargetDataSources(this.targetDataSourceMap);
// 將TargetDataSources中的連接信息放入resolvedDataSources管理
super.afterPropertiesSet();
return Boolean.TRUE;
}
}catch (ClassNotFoundException | SQLException e) {
log.error("---程序報錯---:{}", e.getMessage());
}
return Boolean.FALSE;
}
/**
* 校驗數(shù)據(jù)源是否存在
* @param key 數(shù)據(jù)源保存的key
* @return 返回結(jié)果,true:存在,false:不存在
*/
public boolean existsDataSource(String key){
return Objects.nonNull(this.targetDataSourceMap.get(key));
}
}
在改造后的DynamicDataSource中,我們添加可以一個 private final Map<Object,Object> targetDataSourceMap,這個map會在添加數(shù)據(jù)源的配置文件時將創(chuàng)建的Map數(shù)據(jù)源信息通過DynamicDataSource構(gòu)造方法進(jìn)行初始賦值,即:DateSourceConfig類中的createDynamicDataSource()方法中。
同時我們在該類中添加了一個createDataSource方法,進(jìn)行數(shù)據(jù)源的創(chuàng)建,并添加到map中,再通過super.setTargetDataSources(this.targetDataSourceMap) ;進(jìn)行目標(biāo)數(shù)據(jù)源的重新賦值。
2.5.2.3 動態(tài)添加數(shù)據(jù)源
上述代碼已經(jīng)實現(xiàn)了添加數(shù)據(jù)源的方法,那么我們來模擬通過從數(shù)據(jù)庫表中添加數(shù)據(jù)源,然后我們通過調(diào)用加載數(shù)據(jù)源的方法將數(shù)據(jù)源添加進(jìn)數(shù)據(jù)源Map中。
在主數(shù)據(jù)庫中定義一個數(shù)據(jù)庫表,用于保存數(shù)據(jù)庫信息。
為了方便,我們將之前的從庫錄入到數(shù)據(jù)庫中,修改數(shù)據(jù)庫名稱。
insert into test_db_info(url, username, password,driver_class_name, name)
value ('jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false',
'root','123456','com.mysql.cj.jdbc.Driver','add_slave')
數(shù)據(jù)庫表對應(yīng)的實體、mapper,小伙伴們自行添加。
啟動SpringBoot時添加數(shù)據(jù)源:
/**
* @author: jiangjs
* @description:
* @date: 2023/7/27 16:56
**/
@Component
public class LoadDataSourceRunner implements CommandLineRunner {
@Resource
private DynamicDataSource dynamicDataSource;
@Resource
private TestDbInfoMapper testDbInfoMapper;
@Override
public void run(String... args) throws Exception {
List<TestDbInfo> testDbInfos = testDbInfoMapper.selectList(null);
if (CollectionUtils.isNotEmpty(testDbInfos)) {
List<DataSourceEntity> ds = new ArrayList<>();
for (TestDbInfo testDbInfo : testDbInfos) {
DataSourceEntity sourceEntity = new DataSourceEntity();
BeanUtils.copyProperties(testDbInfo,sourceEntity);
sourceEntity.setKey(testDbInfo.getName());
ds.add(sourceEntity);
}
dynamicDataSource.createDataSource(ds);
}
}
}
經(jīng)過上述SpringBoot啟動后,已經(jīng)將數(shù)據(jù)庫表中的數(shù)據(jù)添加到動態(tài)數(shù)據(jù)源中,我們調(diào)用之前的測試方法,將數(shù)據(jù)源名稱作為參數(shù)傳入看看執(zhí)行結(jié)果。
2.5.2.4 測試

通過測試我們發(fā)現(xiàn)數(shù)據(jù)庫表中的數(shù)據(jù)庫被動態(tài)加入了數(shù)據(jù)源中,小伙伴可以愉快地隨意添加數(shù)據(jù)源了。
到此這篇關(guān)于SpringBoot實現(xiàn)動態(tài)切換數(shù)據(jù)源的示例代碼的文章就介紹到這了,更多相關(guān)SpringBoot動態(tài)切換數(shù)據(jù)源內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 在SpringBoot項目中動態(tài)切換數(shù)據(jù)源和數(shù)據(jù)庫的詳細(xì)步驟
- SpringBoot實現(xiàn)數(shù)據(jù)源動態(tài)切換的最佳姿勢
- SpringBoot項目中如何動態(tài)切換數(shù)據(jù)源、數(shù)據(jù)庫
- SpringBoot實現(xiàn)動態(tài)數(shù)據(jù)源切換的項目實踐
- SpringBoot實現(xiàn)動態(tài)數(shù)據(jù)源切換的方法總結(jié)
- 使用SpringBoot動態(tài)切換數(shù)據(jù)源的實現(xiàn)方式
- Springboot實現(xiàn)多數(shù)據(jù)源切換詳情
- SpringBoot多數(shù)據(jù)源切換實現(xiàn)代碼(Mybaitis)
相關(guān)文章
java實現(xiàn)字符串和數(shù)字轉(zhuǎn)換工具
這篇文章主要為大家詳細(xì)介紹了java實現(xiàn)字符串和數(shù)字轉(zhuǎn)換工具,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-04-04
springBoot集成redis的key,value序列化的相關(guān)問題
這篇文章主要介紹了springBoot集成redis的key,value序列化的相關(guān)問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
servlet之web路徑問題_動力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了servlet之web路徑問題的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07
SpringBoot項目中Druid自動登錄功能實現(xiàn)
Druid是Java語言中最好的數(shù)據(jù)庫連接池,Druid能夠提供強(qiáng)大的監(jiān)控和擴(kuò)展功能,這篇文章主要介紹了SpringBoot項目中Druid自動登錄功能實現(xiàn),需要的朋友可以參考下2024-08-08
Java設(shè)計模式之命令模式CommandPattern詳解
這篇文章主要介紹了Java設(shè)計模式之命令模式CommandPattern詳解,命令模式是把一個請求封裝為一個對象,從而使你可用不同的請求對客戶進(jìn)行參數(shù)化;對請求排隊或記錄請求日志,以及支持可撤銷的操作,需要的朋友可以參考下2023-10-10

