最近在做業(yè)務(wù)需求時(shí),需要從不同的數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)然后寫(xiě)入到當(dāng)前數(shù)據(jù)庫(kù)中,因此涉及到切換數(shù)據(jù)源問(wèn)題。本來(lái)想著使用Mybatis-plus中提供的動(dòng)態(tài)數(shù)據(jù)源SpringBoot的starter:dynamic-datasource-spring-boot-starter來(lái)實(shí)現(xiàn)。
結(jié)果引入后發(fā)現(xiàn)由于之前項(xiàng)目環(huán)境問(wèn)題導(dǎo)致無(wú)法使用。然后研究了下數(shù)據(jù)源切換代碼,決定自己采用ThreadLocal+AbstractRoutingDataSource來(lái)模擬實(shí)現(xiàn)dynamic-datasource-spring-boot-starter中線程數(shù)據(jù)源切換。
1 簡(jiǎn)介
上述提到了ThreadLocal和AbstractRoutingDataSource,我們來(lái)對(duì)其進(jìn)行簡(jiǎn)單介紹下。
ThreadLocal:想必大家必不會(huì)陌生,全稱:thread local variable。主要是為解決多線程時(shí)由于并發(fā)而產(chǎn)生數(shù)據(jù)不一致問(wèn)題。ThreadLocal為每個(gè)線程提供變量副本,確保每個(gè)線程在某一時(shí)間訪問(wèn)到的不是同一個(gè)對(duì)象,這樣做到了隔離性,增加了內(nèi)存,但大大減少了線程同步時(shí)的性能消耗,減少了線程并發(fā)控制的復(fù)雜程度。
ThreadLocal作用:在一個(gè)線程中共享,不同線程間隔離
ThreadLocal原理:ThreadLocal存入值時(shí),會(huì)獲取當(dāng)前線程實(shí)例作為key,存入當(dāng)前線程對(duì)象中的Map中。
AbstractRoutingDataSource:根據(jù)用戶定義的規(guī)則選擇當(dāng)前的數(shù)據(jù)源,
作用:在執(zhí)行查詢之前,設(shè)置使用的數(shù)據(jù)源,實(shí)現(xiàn)動(dòng)態(tài)路由的數(shù)據(jù)源,在每次數(shù)據(jù)庫(kù)查詢操作前執(zhí)行它的抽象方法determineCurrentLookupKey(),決定使用哪個(gè)數(shù)據(jù)源。
2 代碼實(shí)現(xiàn)
程序環(huán)境:
SpringBoot2.4.8
Mybatis-plus3.2.0
Druid1.2.6
lombok1.18.20
commons-lang3 3.10
2.1 實(shí)現(xiàn)ThreadLocal
創(chuàng)建一個(gè)類用于實(shí)現(xiàn)ThreadLocal,主要是通過(guò)get,set,remove方法來(lái)獲取、設(shè)置、刪除當(dāng)前線程對(duì)應(yīng)的數(shù)據(jù)源。
/** *@author:jiangjs *@description: *@date:2023/7/2711:21 **/ publicclassDataSourceContextHolder{ //此類提供線程局部變量。這些變量不同于它們的正常對(duì)應(yīng)關(guān)系是每個(gè)線程訪問(wèn)一個(gè)線程(通過(guò)get、set方法),有自己的獨(dú)立初始化變量的副本。 privatestaticfinalThreadLocalDATASOURCE_HOLDER=newThreadLocal<>(); /** *設(shè)置數(shù)據(jù)源 *@paramdataSourceName數(shù)據(jù)源名稱 */ publicstaticvoidsetDataSource(StringdataSourceName){ DATASOURCE_HOLDER.set(dataSourceName); } /** *獲取當(dāng)前線程的數(shù)據(jù)源 *@return數(shù)據(jù)源名稱 */ publicstaticStringgetDataSource(){ returnDATASOURCE_HOLDER.get(); } /** *刪除當(dāng)前數(shù)據(jù)源 */ publicstaticvoidremoveDataSource(){ DATASOURCE_HOLDER.remove(); } }
2.2 實(shí)現(xiàn)AbstractRoutingDataSource
定義一個(gè)動(dòng)態(tài)數(shù)據(jù)源類實(shí)現(xiàn)AbstractRoutingDataSource,通過(guò)determineCurrentLookupKey方法與上述實(shí)現(xiàn)的ThreadLocal類中的get方法進(jìn)行關(guān)聯(lián),實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)源。
/** *@author:jiangjs *@description:實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)源,根據(jù)AbstractRoutingDataSource路由到不同數(shù)據(jù)源中 *@date:2023/7/2711:18 **/ publicclassDynamicDataSourceextendsAbstractRoutingDataSource{ publicDynamicDataSource(DataSourcedefaultDataSource,Map
上述代碼中,還實(shí)現(xiàn)了一個(gè)動(dòng)態(tài)數(shù)據(jù)源類的構(gòu)造方法,主要是為了設(shè)置默認(rèn)數(shù)據(jù)源,以及以Map保存的各種目標(biāo)數(shù)據(jù)源。其中Map的key是設(shè)置的數(shù)據(jù)源名稱,value則是對(duì)應(yīng)的數(shù)據(jù)源(DataSource)。
2.3 配置數(shù)據(jù)庫(kù)
application.yml中配置數(shù)據(jù)庫(kù)信息:
#設(shè)置數(shù)據(jù)源 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:設(shè)置數(shù)據(jù)源 *@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
通過(guò)配置類,將配置文件中的配置的數(shù)據(jù)庫(kù)信息轉(zhuǎn)換成datasource,并添加到DynamicDataSource中,同時(shí)通過(guò)@Bean將DynamicDataSource注入Spring中進(jìn)行管理,后期在進(jìn)行動(dòng)態(tài)數(shù)據(jù)源添加時(shí),會(huì)用到。
2.4 測(cè)試
在主從兩個(gè)測(cè)試庫(kù)中,分別添加一張表test_user,里面只有一個(gè)字段user_name。
createtabletest_user( user_namevarchar(255)notnullcomment'用戶名' )
在主庫(kù)添加信息:
insertintotest_user(user_name)value('master');
從庫(kù)中添加信息:
insertintotest_user(user_name)value('slave');
我們創(chuàng)建一個(gè)getData的方法,參數(shù)就是需要查詢數(shù)據(jù)的數(shù)據(jù)源名稱。
@GetMapping("/getData.do/{datasourceName}") publicStringgetMasterData(@PathVariable("datasourceName")StringdatasourceName){ DataSourceContextHolder.setDataSource(datasourceName); TestUsertestUser=testUserMapper.selectOne(null); DataSourceContextHolder.removeDataSource(); returntestUser.getUserName(); }
其他的Mapper和實(shí)體類大家自行實(shí)現(xiàn)。
執(zhí)行結(jié)果:
1、傳遞master時(shí):
2、傳遞slave時(shí):
通過(guò)執(zhí)行結(jié)果,我們看到傳遞不同的數(shù)據(jù)源名稱,查詢對(duì)應(yīng)的數(shù)據(jù)庫(kù)是不一樣的,返回結(jié)果也不一樣。
在上述代碼中,我們看到DataSourceContextHolder.setDataSource(datasourceName); 來(lái)設(shè)置了當(dāng)前線程需要查詢的數(shù)據(jù)庫(kù),通過(guò)DataSourceContextHolder.removeDataSource(); 來(lái)移除當(dāng)前線程已設(shè)置的數(shù)據(jù)源。使用過(guò)Mybatis-plus動(dòng)態(tài)數(shù)據(jù)源的小伙伴,應(yīng)該還記得我們?cè)谑褂们袚Q數(shù)據(jù)源時(shí)會(huì)使用到DynamicDataSourceContextHolder.push(String ds); 和DynamicDataSourceContextHolder.poll(); 這兩個(gè)方法,翻看源碼我們會(huì)發(fā)現(xiàn)其實(shí)就是在使用ThreadLocal時(shí)使用了棧,這樣的好處就是能使用多數(shù)據(jù)源嵌套,這里就不帶大家實(shí)現(xiàn)了,有興趣的小伙伴可以看看Mybatis-plus中動(dòng)態(tài)數(shù)據(jù)源的源碼。
注:?jiǎn)?dòng)程序時(shí),小伙伴不要忘記將SpringBoot自動(dòng)添加數(shù)據(jù)源進(jìn)行排除哦,否則會(huì)報(bào)循環(huán)依賴問(wèn)題。
@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
2.5 優(yōu)化調(diào)整
2.5.1 注解切換數(shù)據(jù)源
在上述中,雖然已經(jīng)實(shí)現(xiàn)了動(dòng)態(tài)切換數(shù)據(jù)源,但是我們會(huì)發(fā)現(xiàn)如果涉及到多個(gè)業(yè)務(wù)進(jìn)行切換數(shù)據(jù)源的話,我們就需要在每一個(gè)實(shí)現(xiàn)類中添加這一段代碼。
說(shuō)到這有小伙伴應(yīng)該就會(huì)想到使用注解來(lái)進(jìn)行優(yōu)化,接下來(lái)我們來(lái)實(shí)現(xiàn)一下。
2.5.1.1 定義注解
我們就用mybatis動(dòng)態(tài)數(shù)據(jù)源切換的注解: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 實(shí)現(xiàn)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,通過(guò)ProceedingJoinPoint獲取注解信息,拿到注解傳遞值,然后設(shè)置當(dāng)前線程的數(shù)據(jù)源。對(duì)aop不了解的小伙伴可以自行g(shù)oogle或百度。
2.5.1.3 測(cè)試
添加兩個(gè)測(cè)試方法:
@GetMapping("/getMasterData.do") publicStringgetMasterData(){ TestUsertestUser=testUserMapper.selectOne(null); returntestUser.getUserName(); } @GetMapping("/getSlaveData.do") @DS("slave") publicStringgetSlaveData(){ TestUsertestUser=testUserMapper.selectOne(null); returntestUser.getUserName(); }
由于@DS中設(shè)置的默認(rèn)值是:master,因此在調(diào)用主數(shù)據(jù)源時(shí),可以不用進(jìn)行添加。
執(zhí)行結(jié)果:
1、調(diào)用getMasterData.do方法:
2、調(diào)用getSlaveData.do方法:
通過(guò)執(zhí)行結(jié)果,我們通過(guò)@DS也進(jìn)行了數(shù)據(jù)源的切換,實(shí)現(xiàn)了Mybatis-plus動(dòng)態(tài)切換數(shù)據(jù)源中的通過(guò)注解切換數(shù)據(jù)源的方式。
2.5.2 動(dòng)態(tài)添加數(shù)據(jù)源
業(yè)務(wù)場(chǎng)景 :有時(shí)候我們的業(yè)務(wù)會(huì)要求我們從保存有其他數(shù)據(jù)源的數(shù)據(jù)庫(kù)表中添加這些數(shù)據(jù)源,然后再根據(jù)不同的情況切換這些數(shù)據(jù)源。
因此我們需要改造下DynamicDataSource來(lái)實(shí)現(xiàn)動(dòng)態(tài)加載數(shù)據(jù)源。
2.5.2.1 數(shù)據(jù)源實(shí)體
/** *@author:jiangjs *@description:數(shù)據(jù)源實(shí)體 *@date:2023/7/2715:55 **/ @Data @Accessors(chain=true) publicclassDataSourceEntity{ /** *數(shù)據(jù)庫(kù)地址 */ privateStringurl; /** *數(shù)據(jù)庫(kù)用戶名 */ privateStringuserName; /** *密碼 */ privateStringpassWord; /** *數(shù)據(jù)庫(kù)驅(qū)動(dòng) */ privateStringdriverClassName; /** *數(shù)據(jù)庫(kù)key,即保存Map中的key */ privateStringkey; }
實(shí)體中定義數(shù)據(jù)源的一般信息,同時(shí)定義一個(gè)key用于作為DynamicDataSource中Map中的key。
2.5.2.2 修改DynamicDataSource
/** *@author:jiangjs *@description:實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)源,根據(jù)AbstractRoutingDataSource路由到不同數(shù)據(jù)源中 *@date:2023/7/2711:18 **/ @Slf4j publicclassDynamicDataSourceextendsAbstractRoutingDataSource{ privatefinalMap
在改造后的DynamicDataSource中,我們添加可以一個(gè) private final Map
同時(shí)我們?cè)谠擃愔刑砑恿艘粋€(gè)createDataSource方法,進(jìn)行數(shù)據(jù)源的創(chuàng)建,并添加到map中,再通過(guò)super.setTargetDataSources(this.targetDataSourceMap) ;進(jìn)行目標(biāo)數(shù)據(jù)源的重新賦值。
2.5.2.3 動(dòng)態(tài)添加數(shù)據(jù)源
上述代碼已經(jīng)實(shí)現(xiàn)了添加數(shù)據(jù)源的方法,那么我們來(lái)模擬通過(guò)從數(shù)據(jù)庫(kù)表中添加數(shù)據(jù)源,然后我們通過(guò)調(diào)用加載數(shù)據(jù)源的方法將數(shù)據(jù)源添加進(jìn)數(shù)據(jù)源Map中。
在主數(shù)據(jù)庫(kù)中定義一個(gè)數(shù)據(jù)庫(kù)表,用于保存數(shù)據(jù)庫(kù)信息。
createtabletest_db_info( idintauto_incrementprimarykeynotnullcomment'主鍵Id', urlvarchar(255)notnullcomment'數(shù)據(jù)庫(kù)URL', usernamevarchar(255)notnullcomment'用戶名', passwordvarchar(255)notnullcomment'密碼', driver_class_namevarchar(255)notnullcomment'數(shù)據(jù)庫(kù)驅(qū)動(dòng)' namevarchar(255)notnullcomment'數(shù)據(jù)庫(kù)名稱' )
為了方便,我們將之前的從庫(kù)錄入到數(shù)據(jù)庫(kù)中,修改數(shù)據(jù)庫(kù)名稱。
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')
數(shù)據(jù)庫(kù)表對(duì)應(yīng)的實(shí)體、mapper,小伙伴們自行添加。
啟動(dòng)SpringBoot時(shí)添加數(shù)據(jù)源:
/** *@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); } } }
經(jīng)過(guò)上述SpringBoot啟動(dòng)后,已經(jīng)將數(shù)據(jù)庫(kù)表中的數(shù)據(jù)添加到動(dòng)態(tài)數(shù)據(jù)源中,我們調(diào)用之前的測(cè)試方法,將數(shù)據(jù)源名稱作為參數(shù)傳入看看執(zhí)行結(jié)果。
2.5.2.4 測(cè)試
通過(guò)測(cè)試我們發(fā)現(xiàn)數(shù)據(jù)庫(kù)表中的數(shù)據(jù)庫(kù)被動(dòng)態(tài)加入了數(shù)據(jù)源中,小伙伴可以愉快地隨意添加數(shù)據(jù)源了。
好了,今天就跟大家嘮叨到這,希望我的叨叨讓大家對(duì)于動(dòng)態(tài)切換數(shù)據(jù)源的方式能夠有更深地理解。
審核編輯:湯梓紅
-
內(nèi)存
+關(guān)注
關(guān)注
8文章
3019瀏覽量
74002 -
數(shù)據(jù)源
+關(guān)注
關(guān)注
1文章
63瀏覽量
9676 -
SpringBoot
+關(guān)注
關(guān)注
0文章
173瀏覽量
177
原文標(biāo)題:SpringBoot 實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)源,這樣做才更優(yōu)雅!
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論