最近在做業務需求時,需要從不同的數據庫中獲取數據然后寫入到當前數據庫中,因此涉及到切換數據源問題。本來想著使用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方法來獲取、設置、刪除當前線程對應的數據源。
/** *@author:jiangjs *@description: *@date:2023/7/2711:21 **/ publicclassDataSourceContextHolder{ //此類提供線程局部變量。這些變量不同于它們的正常對應關系是每個線程訪問一個線程(通過get、set方法),有自己的獨立初始化變量的副本。 privatestaticfinalThreadLocalDATASOURCE_HOLDER=newThreadLocal<>(); /** *設置數據源 *@paramdataSourceName數據源名稱 */ publicstaticvoidsetDataSource(StringdataSourceName){ DATASOURCE_HOLDER.set(dataSourceName); } /** *獲取當前線程的數據源 *@return數據源名稱 */ publicstaticStringgetDataSource(){ returnDATASOURCE_HOLDER.get(); } /** *刪除當前數據源 */ publicstaticvoidremoveDataSource(){ DATASOURCE_HOLDER.remove(); } }
2.2 實現AbstractRoutingDataSource
定義一個動態數據源類實現AbstractRoutingDataSource,通過determineCurrentLookupKey方法與上述實現的ThreadLocal類中的get方法進行關聯,實現動態切換數據源。
/** *@author:jiangjs *@description:實現動態數據源,根據AbstractRoutingDataSource路由到不同數據源中 *@date:2023/7/2711:18 **/ publicclassDynamicDataSourceextendsAbstractRoutingDataSource{ publicDynamicDataSource(DataSourcedefaultDataSource,Map
上述代碼中,還實現了一個動態數據源類的構造方法,主要是為了設置默認數據源,以及以Map保存的各種目標數據源。其中Map的key是設置的數據源名稱,value則是對應的數據源(DataSource)。
2.3 配置數據庫
application.yml中配置數據庫信息:
#設置數據源 spring: datasource: type:com.alibaba.druid.pool.DruidDataSource druid: master: url:jdbc//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//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/2711:34 **/ @Configuration publicclassDateSourceConfig{ @Bean @ConfigurationProperties("spring.datasource.druid.master") publicDataSourcemasterDataSource(){ returnDruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.druid.slave") publicDataSourceslaveDataSource(){ returnDruidDataSourceBuilder.create().build(); } @Bean(name="dynamicDataSource") @Primary publicDynamicDataSourcecreateDynamicDataSource(){ Map
通過配置類,將配置文件中的配置的數據庫信息轉換成datasource,并添加到DynamicDataSource中,同時通過@Bean將DynamicDataSource注入Spring中進行管理,后期在進行動態數據源添加時,會用到。
2.4 測試
在主從兩個測試庫中,分別添加一張表test_user,里面只有一個字段user_name。
createtabletest_user( user_namevarchar(255)notnullcomment'用戶名' )
在主庫添加信息:
insertintotest_user(user_name)value('master');
從庫中添加信息:
insertintotest_user(user_name)value('slave');
我們創建一個getData的方法,參數就是需要查詢數據的數據源名稱。
@GetMapping("/getData.do/{datasourceName}") publicStringgetMasterData(@PathVariable("datasourceName")StringdatasourceName){ DataSourceContextHolder.setDataSource(datasourceName); TestUsertestUser=testUserMapper.selectOne(null); DataSourceContextHolder.removeDataSource(); returntestUser.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 注解切換數據源
在上述中,雖然已經實現了動態切換數據源,但是我們會發現如果涉及到多個業務進行切換數據源的話,我們就需要在每一個實現類中添加這一段代碼。
說到這有小伙伴應該就會想到使用注解來進行優化,接下來我們來實現一下。
2.5.1.1 定義注解
我們就用mybatis動態數據源切換的注解:DS,代碼如下:
/** *@author:jiangjs *@description: *@date:2023/7/2714:39 **/ @Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public@interfaceDS{ Stringvalue()default"master"; }
2.5.1.2 實現aop
@Aspect @Component @Slf4j publicclassDSAspect{ @Pointcut("@annotation(com.jiashn.dynamic_datasource.dynamic.aop.DS)") publicvoiddynamicDataSource(){} @Around("dynamicDataSource()") publicObjectdatasourceAround(ProceedingJoinPointpoint)throwsThrowable{ MethodSignaturesignature=(MethodSignature)point.getSignature(); Methodmethod=signature.getMethod(); DSds=method.getAnnotation(DS.class); if(Objects.nonNull(ds)){ DataSourceContextHolder.setDataSource(ds.value()); } try{ returnpoint.proceed(); }finally{ DataSourceContextHolder.removeDataSource(); } } }
代碼使用了@Around,通過ProceedingJoinPoint獲取注解信息,拿到注解傳遞值,然后設置當前線程的數據源。對aop不了解的小伙伴可以自行google或百度。
2.5.1.3 測試
添加兩個測試方法:
@GetMapping("/getMasterData.do") publicStringgetMasterData(){ TestUsertestUser=testUserMapper.selectOne(null); returntestUser.getUserName(); } @GetMapping("/getSlaveData.do") @DS("slave") publicStringgetSlaveData(){ TestUsertestUser=testUserMapper.selectOne(null); returntestUser.getUserName(); }
由于@DS中設置的默認值是:master,因此在調用主數據源時,可以不用進行添加。
執行結果:
1、調用getMasterData.do方法:
2、調用getSlaveData.do方法:
通過執行結果,我們通過@DS也進行了數據源的切換,實現了Mybatis-plus動態切換數據源中的通過注解切換數據源的方式。
2.5.2 動態添加數據源
業務場景 :有時候我們的業務會要求我們從保存有其他數據源的數據庫表中添加這些數據源,然后再根據不同的情況切換這些數據源。
因此我們需要改造下DynamicDataSource來實現動態加載數據源。
2.5.2.1 數據源實體
/** *@author:jiangjs *@description:數據源實體 *@date:2023/7/2715:55 **/ @Data @Accessors(chain=true) publicclassDataSourceEntity{ /** *數據庫地址 */ privateStringurl; /** *數據庫用戶名 */ privateStringuserName; /** *密碼 */ privateStringpassWord; /** *數據庫驅動 */ privateStringdriverClassName; /** *數據庫key,即保存Map中的key */ privateStringkey; }
實體中定義數據源的一般信息,同時定義一個key用于作為DynamicDataSource中Map中的key。
2.5.2.2 修改DynamicDataSource
/** *@author:jiangjs *@description:實現動態數據源,根據AbstractRoutingDataSource路由到不同數據源中 *@date:2023/7/2711:18 **/ @Slf4j publicclassDynamicDataSourceextendsAbstractRoutingDataSource{ privatefinalMap
在改造后的DynamicDataSource中,我們添加可以一個 private final Map
同時我們在該類中添加了一個createDataSource方法,進行數據源的創建,并添加到map中,再通過super.setTargetDataSources(this.targetDataSourceMap) ;進行目標數據源的重新賦值。
2.5.2.3 動態添加數據源
上述代碼已經實現了添加數據源的方法,那么我們來模擬通過從數據庫表中添加數據源,然后我們通過調用加載數據源的方法將數據源添加進數據源Map中。
在主數據庫中定義一個數據庫表,用于保存數據庫信息。
createtabletest_db_info( idintauto_incrementprimarykeynotnullcomment'主鍵Id', urlvarchar(255)notnullcomment'數據庫URL', usernamevarchar(255)notnullcomment'用戶名', passwordvarchar(255)notnullcomment'密碼', driver_class_namevarchar(255)notnullcomment'數據庫驅動' namevarchar(255)notnullcomment'數據庫名稱' )
為了方便,我們將之前的從庫錄入到數據庫中,修改數據庫名稱。
insertintotest_db_info(url,username,password,driver_class_name,name) value('jdbc//xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false', 'root','123456','com.mysql.cj.jdbc.Driver','add_slave')
數據庫表對應的實體、mapper,小伙伴們自行添加。
啟動SpringBoot時添加數據源:
/** *@author:jiangjs *@description: *@date:2023/7/2716:56 **/ @Component publicclassLoadDataSourceRunnerimplementsCommandLineRunner{ @Resource privateDynamicDataSourcedynamicDataSource; @Resource privateTestDbInfoMappertestDbInfoMapper; @Override publicvoidrun(String...args)throwsException{ ListtestDbInfos=testDbInfoMapper.selectList(null); if(CollectionUtils.isNotEmpty(testDbInfos)){ List ds=newArrayList<>(); for(TestDbInfotestDbInfo:testDbInfos){ DataSourceEntitysourceEntity=newDataSourceEntity(); BeanUtils.copyProperties(testDbInfo,sourceEntity); sourceEntity.setKey(testDbInfo.getName()); ds.add(sourceEntity); } dynamicDataSource.createDataSource(ds); } } }
經過上述SpringBoot啟動后,已經將數據庫表中的數據添加到動態數據源中,我們調用之前的測試方法,將數據源名稱作為參數傳入看看執行結果。
2.5.2.4 測試
通過測試我們發現數據庫表中的數據庫被動態加入了數據源中,小伙伴可以愉快地隨意添加數據源了。
好了,今天就跟大家嘮叨到這,希望我的叨叨讓大家對于動態切換數據源的方式能夠有更深地理解。
審核編輯:湯梓紅
-
內存
+關注
關注
8文章
3117瀏覽量
75120 -
數據源
+關注
關注
1文章
65瀏覽量
9897 -
SpringBoot
+關注
關注
0文章
175瀏覽量
359
原文標題:SpringBoot 實現動態切換數據源,這樣做才更優雅!
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
Mybatis 攔截器實現單數據源內多數據庫切換
LabView動態創建數據源的方法
【測試之王LabVIEW】注冊表應用一:動態注冊數據源
基于LDA主題模型進行數據源選擇方法

Deep Web數據源選擇和集成方法

數據倉庫入門之創建數據源

評論