導讀
- MyBatis多數據源配置與使用
- 其一
- 其二
- 1. 引依賴
- 2. 配置文件
- 3. 編寫測試代碼
- 4. 自定義DynamicDataSource類
- 5. DataSourceConfig配置類
- 6. AOP與ThreadLocal結合
- 7. 引入AOP依賴
- 8. DataSourceContextHolder
- 9. 自定義注解@UseDB
- 10. 創建切面類UseDBAspect
- 11. 修改DynamicDataSource
- 12. 簡單測試一下
- 13. 未完
- 14. 結合棧的使用
- 15. 修改DataSourceContextHolder
- 16. 最后小坑
MyBatis多數據源配置與使用
前言:MyBatis默認情況下只能在application配置文件中配置單數據源,但有一些開發場景可能有多數據源的需求,這需要做一些額外的配置。
查了一下Mybatis多數據源的解決方案,主要有兩種方式:
其一
利用MyBatis的@MapperScan注解,該注解除了標注掃描路徑外,還能給掃描到的mapper文件的dao操作指定sqlSessionFactoryRef屬性指定使用的SqlSessionFactory,此時我們就可以構建不同源的SqlSessionFactory,從而實現不同的mapper文件對應不同的數據源操作。
這種方式簡單易懂,創建對應的SqlSessionFactory即可,缺點是需要為每個數據源維護對應的mapper文件。這里不詳細描述這種方式。
其二
第二種方式是利用springboot自身的AbstractRoutingDataSource,AbstractRoutingDataSource是一個抽象類,其中維護了一個Map屬性,該Map是用于存儲多個數據源,通過不同的key獲取對應的數據源。另外提供determineCurrentLookupKey抽象方法,供給用戶自定義獲取鍵的方式。例如我們兩個數據庫,db1和db2,當我們想用db1時,只需要讓determineCurrentLookupKey方法獲取到db1的key就行,db2同理。下面說下詳細編碼過程:
1. 引依賴
無需額外依賴,springboot,mybatis,mysql驅動即可,注意的是如果springboot版本過高,則可能需要升級其中的mybatis-spring版本,否則報錯
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- springboot版本過高,需要升級其中的mybatis-spring版本,否則報錯 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.2</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId><version>3.0.3</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.19</version></dependency></dependencies>
2. 配置文件
配置文件中定義數據源的信息,需要注意的是,在單數據源中,連接數據庫參數時,使用的key是url,但在多數據源中,默認使用的是jdbc-url。(實際上我們也可以隨便定義,但需要我們自己讀取配置封裝DataSource,后面會講到)
spring:application:name: MultiSourceMyBatis# datasource配置文件如下datasource:# 數據源1db1:username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://127.0.0.1/inote?userUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai# 數據源2db2:username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://111.111.111.111/inote?userUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
3. 編寫測試代碼
測試代碼的部分省略,就是controller,service,dao常規流程
4. 自定義DynamicDataSource類
創建DynamicDataSource類,繼承AbstractRoutingDataSource類,實現determineCurrentLookupKey抽象方法,determineCurrentLookupKey方法就是如何獲取DataSource的key的方法。通過不同的key獲取對應的數據源。該方法的具體實現我們暫時留白,下面會再做修改
public class DynamicDataSource extends AbstractRoutingDataSource {/*** 獲取數據源key的方式,要使用哪個數據源,是通過數據源key選擇的,這個key是數據源map中的key*/@Overrideprotected Object determineCurrentLookupKey() {return "db1";}
}
5. DataSourceConfig配置類
DataSourceConfig這個類的主要作用是將我們自定義DynamicDataSource類的實例對象交由spring bean管理,由容器裝配與調用。而在這之前,我們還需要給DynamicDataSource設置DataSource的map(也就是將多個DataSource添加到DynamicDataSource中)。
@Configuration
public class DataSourceConfig {@AutowiredEnvironment environment; // 用于讀取application.yml文件配置/*** 構建兩個數據庫源,交由spring管理,但其實直接創建也無妨,注意保證創建相同配置的DataSource只有一個就行*/@Beanpublic DataSource db1(){HikariDataSource dataSource = new HikariDataSource();dataSource.setDriverClassName(environment.getProperty("spring.datasource.db1.driver-class-name"));dataSource.setJdbcUrl(environment.getProperty("spring.datasource.db1.jdbc-url"));dataSource.setUsername(environment.getProperty("spring.datasource.db1.username"));dataSource.setPassword(environment.getProperty("spring.datasource.db1.password"));return dataSource;}@Beanpublic DataSource db2(){HikariDataSource dataSource = new HikariDataSource();dataSource.setDriverClassName(environment.getProperty("spring.datasource.db2.driver-class-name"));dataSource.setJdbcUrl(environment.getProperty("spring.datasource.db2.jdbc-url"));dataSource.setUsername(environment.getProperty("spring.datasource.db2.username"));dataSource.setPassword(environment.getProperty("spring.datasource.db2.password"));return dataSource;}
// /**
// * 實際上創建DataSource的方式可以用以下代碼替代,但是需要注意的是配置文件中的數據庫連接參數要改為jdbc-url
// */
// @ConfigurationProperties(prefix = "spring.datasource.db1")
// @Bean
// public DataSource db1(){
// return DataSourceBuilder.create().build();
// }/*** 創建DynamicDataSource,并將db1,db2添加進去。*/@Bean("dynamicDataSource")@Primary // 該注解表示如果有多個相同bean,首選這個public DataSource dynamicDataSource(@Qualifier("db1") DataSource db1,@Qualifier("db2") DataSource db2){DynamicDataSource dynamicDataSource = new DynamicDataSource();//默認數據源,如果determineCurrentLookupKey方法獲取到的key不在列表中,則走默認的datasourcedynamicDataSource.setDefaultTargetDataSource(db1);Map<Object,Object> map = new HashMap<>();map.put("db1",db1);map.put("db2",db2);dynamicDataSource.setTargetDataSources(map);return dynamicDataSource;}
}
至此,配置就完成了,此時我們可以通過上面的determineCurrentLookupKey方法指定我們想使用的數據源。
這時候就會有人問了,這也沒完成啊,determineCurrentLookupKey方法中寫死了數據庫的key,怎么做到數據庫切換?
剛才說了,determineCurrentLookupKey方法留白了,關鍵就是怎么動態切換要使用的數據庫的key,就的改寫determineCurrentLookupKey方法。下面就展開說說。
6. AOP與ThreadLocal結合
我們想實現多數據源,目的肯定是希望不同用戶,或者不同操作同時進行時能夠使用不同的數據庫,而不是同一時刻只有一個數據源起作用,因而多線程下,相同操作對不同資源進行訪問,首先想到的是ThreadLocal。如果在用戶請求進來后,我們為其配置對應數據庫源的key,然后在determineCurrentLookupKey中通過ThreadLocal獲取到key,OK,萬事大吉。
但……,我們給一個線程創建同一個數據源,我們需要怎么去創建,創建的時機是怎樣的?基于編碼習慣,我們肯定希望的是通過注解的方式做方法增強。
“對啊,AOP,ThreadLocal+AOP,在service層方法執行前捕獲方法,然后通過ThreadLocal設置數據源,后續就能使用該數據源源進行sql操作了,你真聰明”。
7. 引入AOP依賴
<!-- aop依賴 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
8. DataSourceContextHolder
創建一個線程上下文工具類DataSourceContextHolder,該類主要作用是給線程創建ThreadLocal,然后實現ThreadLocal的getter,setter以及清除工作。
public class DataSourceContextHolder {private static ThreadLocal<String> dataSourceKey = new ThreadLocal<>();public static void setDataSourceKey(String key){dataSourceKey.set(key);}public static String getDataSourceKey(){return dataSourceKey.get();}public static void clear(){dataSourceKey.remove();}}
9. 自定義注解@UseDB
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseDB {/*** 要使用的數據源的key*/String value();
}
10. 創建切面類UseDBAspect
在代理方法執行前設置數據庫源,方法執行后移除數據庫源
@Aspect
@Component
public class UseDBAspect {/*** 定義切面*/@Pointcut(value = "@annotation(com.example.multisourcemybatis.announce.UseDB)")private void getAnnounce(){}/*** 環繞通知* @param joinPoint 切點,就是被注解的目標方法*/@Around("getAnnounce()")public Object logPostMapping(ProceedingJoinPoint joinPoint) throws Throwable {// 獲取自定義注解中的value值MethodSignature signature = (MethodSignature) joinPoint.getSignature();UseDB annotation = signature.getMethod().getAnnotation(UseDB.class);String dataSourceKey = annotation.value();// 將dataSource的key設置到ThreadLocalDataSourceContextHolder.setDataSourceKey(dataSourceKey);// 執行目標方法,也就是service方法Object result = joinPoint.proceed();// 執行方法后,記得清除ThreadLocal,避免內存泄漏DataSourceContextHolder.clear();// 返回方法返回值return result;}}
11. 修改DynamicDataSource
補充DynamicDataSource的determineCurrentLookupKey方法,也就是如何獲得key的方法,改為從ThreadLocal中獲取即可
public class DynamicDataSource extends AbstractRoutingDataSource {/*** 獲取數據源key的方式,要使用哪個數據源,是通過數據源key選擇的,這個key是數據源map中的key*/@Overrideprotected Object determineCurrentLookupKey() {return DataSourceContextHolder.getDataSourceKey();}}
12. 簡單測試一下
service方法
@UseDB("db1")public void addInDB1(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}@UseDB("db2")public void addInDB2(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}
controller方法
@PostMapping("add")public Result add(UserInfo userInfo) throws Exception {userInfoService.addInDB1(userInfo);userInfoService.addInDB2(userInfo);return ResultUtils.success();}
測試結果:
兩個數據庫分別插入一條數據,符合預期
13. 未完
“你這例子確實實現了通過注解方式實現數據源的切換,但是好像有點問題,你測試的例子是從controller中分別執行兩個service方法(被自定義注解@UseDB標注的方法),但在實際開發中,我不確保總是從controller中調用,萬一我在一個service中調用另一個service,而且在調用完另一個service后還需要進行數據庫操作,這樣的話就出問題了,在調用內層service的時候,我的ThreadLocal值已經被覆蓋,并且內層service執行完后還進行了清除ThreadLocal,也就是說外層service設置的數據源已經沒了,等到后面再執行dao操作時,會走默認的數據源,而不是@UseDB標注的數據源。這……是bug啊”
是的,理想狀態下我們認為一個service不調用另一個service,但如果確實調用了,就可能出現bug,但也不是不能解決,那我們就針對性修改下吧
14. 結合棧的使用
我們要實現的效果是,外層方法使用外層數據源,內層方法使用內層方法數據源,如果還有內層的內層方法,使用內層的內層的數據源。然后方法執行完后一步一步彈出,但不影響相對外層的數據源。
有沒有很熟悉,這就是棧啊,先進后出,我們使用棧來存儲數據源的key,當調用內層方法后pop掉就行了,這樣外層方法依舊能獲取到外層的數據源key。
15. 修改DataSourceContextHolder
只修改DataSourceContextHolder,修改setter,getter以及clear方法,適配stack。
public class DataSourceContextHolder {private static ThreadLocal<Stack<String>> dataSourceKey = new ThreadLocal<>();/*** 將DataSource的key添加到ThreadLocal的Stack中,效果等同直接交給ThreadLocal* @param key DataSource的key*/public static void setDataSourceKey(String key){// 判斷stack是否為空,在初始狀態下stack == nullif (dataSourceKey.get()==null){dataSourceKey.set(new Stack<String>());}// 將DataSource的key添加到stack中dataSourceKey.get().push(key);}/*** 獲取ThreadLocal中Stack最后添加進的key,效果等同獲取當前DataSource的key* @return DataSource的key*/public static String getDataSourceKey(){// 注意,我們獲取DataSource時不能采用pop方法,因為我們不能保證一個方法中只有一個數據庫操作,// 如果直接pop,則會導致同一個方法后續數據庫操作使用錯誤的數據源return dataSourceKey.get().peek();}/*** 將DataSource的key刪除,但是不一定刪除ThreadLocal,只有最后一個key配Stack踢出后才刪除ThreadLocal*/public static void clear(){dataSourceKey.get().pop();// 如果此時棧中沒有數據了,則將ThreadLocal清除if (dataSourceKey.get().empty()) {dataSourceKey.remove();}}/*** 額外再寫個方法,無論如何都清除ThreadLocal,避免異常問題,沒有將棧全部踢出,導致ThreadLocal內存泄漏* 建議在servlet攔截器中調用清除,afterCompletion中調用。*/public static void clearWhatever(){dataSourceKey.remove();}}
16. 最后小坑
這個不是上面代碼的坑,而是AOP實現代理時,類的內部調用默認不走代理方法,也就是說,上面service的addInDB1和addInDB2方法,如果在addInDB1中直接調用或通過this調用addInDB2,如下
@UseDB("db1")public void addInDB1(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);// 直接調用addInDB2this.addInDB2(userInfo);}@UseDB("db2")public void addInDB2(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}
上述代碼中this.addInDB2(userInfo);
默認不走AOP動態代理,也就會導致addDB2方法用的依然是db1數據源這是不符合我們預期的,要解決這個問題,也就是走動態代理,我們要:
- 開啟exposeProxy=true的配置,將類內部引用也走AOP代理
在啟動類上標注
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // 允許類內獲取當前實例的代理
public class MultiSourceMyBatisApplication {public static void main(String[] args) {SpringApplication.run(MultiSourceMyBatisApplication.class, args);}}
- 獲取代理對象,通過代理對象調用
@UseDB("db1")public void addInDB1(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);// 通過AopContext獲取當前實例的代理對象UserInfoService userInfoService = (UserInfoService) AopContext.currentProxy();userInfoService.addInDB2(userInfo);}@UseDB("db2")public void addInDB2(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}
至此全篇完。