MyBatis多數據源配置與使用,基于ThreadLocal+AOP

導讀

  • 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數據源這是不符合我們預期的,要解決這個問題,也就是走動態代理,我們要:

  1. 開啟exposeProxy=true的配置,將類內部引用也走AOP代理

在啟動類上標注

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)		// 允許類內獲取當前實例的代理
public class MultiSourceMyBatisApplication {public static void main(String[] args) {SpringApplication.run(MultiSourceMyBatisApplication.class, args);}}
  1. 獲取代理對象,通過代理對象調用
    @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);}

至此全篇完。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/16949.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/16949.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/16949.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

jQuery里添加事件 (代碼)

直接上代碼 <!DOCTYPE html> <html><head></head><body><input type"text" placeholder"城市" id"city" /><input type"button" value"添加" id"btnAdd" /><ul id…

PTA 計算矩陣兩個對角線之和

計算一個nn矩陣兩個對角線之和。 輸入格式: 第一行輸入一個整數n(0<n≤10)&#xff0c;第二行至第n1行&#xff0c;每行輸入n個整數&#xff0c;每行第一個數前沒有空格&#xff0c;每行的每個數之間各有一個空格。 輸出格式: 兩條對角線元素和&#xff0c;輸出格式見樣例…

Android存儲系統成長記

用心堅持輸出易讀、有趣、有深度、高質量、體系化的技術文章 本文概要 您一定使用過Context的getFileStreamPath方法或者Environment的getExternalStoragePublicDirectory方法&#xff0c;甚至還有別的方法把數據存儲到文件中&#xff0c;這些都是存儲系統提供的服務&#x…

PTA 判斷兩個矩陣相等

Peter得到兩個n行m列矩陣&#xff0c;她想知道兩個矩陣是否相等&#xff0c;請你用“Yes”&#xff0c;“No”回答她&#xff08;兩個矩陣相等指的是兩個矩陣對應元素都相等&#xff09;。 輸入格式: 第一行輸入整數n和m&#xff0c;表示兩個矩陣的行與列&#xff0c;用空格隔…

修改元組元素

自學python如何成為大佬(目錄):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 場景模擬&#xff1a;伊米咖啡館&#xff0c;由于麝香貓咖啡需求量較大&#xff0c;庫存不足&#xff0c;店長想把它換成拿鐵咖啡。 實例08 將麝香貓…

chrome瀏覽器驅動下載

跑自動化的時候&#xff0c;需要打開谷歌瀏覽器&#xff0c;這個時候提示瀏覽器驅動找不到咋辦呢&#xff1f; 1、網上搜索找到了這篇文章&#xff1a;https://www.cnblogs.com/laoluoits/p/17710501.html&#xff1b;按照文章介紹&#xff0c; 首先找到&#xff1a;CNPM Bin…

D - Permutation Subsequence(AtCoder Beginner Contest 352)

題目鏈接: D - Permutation Subsequence (atcoder.jp) 題目大意&#xff1a; 分析&#xff1a; 相對于是記錄一下每個數的位置 然后再長度為k的區間進行移動 然后看最大的pos和最小的pos的最小值是多少 有點類似于滑動窗口 用到了java里面的 TreeSet和Map TreeSet存的是數…

解決 Spring Boot 應用啟動失敗的問題:Unexpected end of file from server

解決 Spring Boot 應用啟動失敗的問題&#xff1a;Unexpected end of file from server 博主貓頭虎的技術世界 &#x1f31f; 歡迎來到貓頭虎的博客 — 探索技術的無限可能&#xff01; 專欄鏈接&#xff1a; &#x1f517; 精選專欄&#xff1a; 《面試題大全》 — 面試準備的…

Spring AOP失效的場景事務失效的場景

場景一&#xff1a;使用this調用被增強的方法 下面是一個類里面的一個增強方法 Service public class MyService implements CommandLineRunner {private MyService myService;public void performTask(int x) {System.out.println("Executing performTask method&quo…

爬蟲學習--15.進程與線程(2)

線程鎖 當多個線程幾乎同時修改某一個共享數據的時候&#xff0c;需要進行同步控制 某個線程要更改共享數據時&#xff0c;先將其鎖定&#xff0c;此時資源的狀態為"鎖定",其他線程不能改變&#xff0c;只到該線程釋放資源&#xff0c;將資源的狀態變成"非鎖定…

Linux如何設置共享文件夾

打開虛擬機->菜單->虛擬機設置->選項->共享文件夾->總是啟用。點擊添加按鈕->彈出添加向導->點擊瀏覽按鈕&#xff0c;從windows中選擇一個文件夾&#xff0c;確定即可。

[Windows] GIF動畫、動圖制作神器 ScreenToGif(免費)

ScreenToGif 是開源免費的 Gif 動畫錄制工具&#xff0c;小巧原生單文件&#xff0c;功能很實用。它有錄制屏幕、錄制攝像頭、錄制畫板、圖像編輯器等功能&#xff0c;可以將屏幕任何區域及操作過程錄制成 GIF 格式的動態圖像。保存前還可對 GIF 圖像編輯優化&#xff0c;支持自…

末日設計1.00

故事背景: 在不遠的未來&#xff0c;世界陷入了末日危機。資源枯竭、社會秩序崩潰&#xff0c;幸存者們為了生存&#xff0c;不得不拿起武器爭奪每一寸土地和每一口食物。在這個混亂的世界中&#xff0c;你是一名傳奇狙擊手&#xff0c;憑借超凡的射擊技巧和生存智慧&#xff0…

研二學妹面試字節,竟倒在了ThreadLocal上,這是不要應屆生還是不要女生啊?

一、寫在開頭 今天和一個之前研二的學妹聊天&#xff0c;聊及她上周面試字節的情況&#xff0c;著實感受到了Java后端現在找工作的壓力啊&#xff0c;記得在18&#xff0c;19年的時候&#xff0c;研究生計算機專業的學生&#xff0c;背背八股文找個Java開發工作毫無問題&#x…

本地圖形客戶端查看git提交歷史 使用 TortoiseGit

要在本地查看提交記錄和修改歷史&#xff0c;可以使用 TortoiseGit 和 Git-SCM。這兩個工具都提供了強大的功能來管理和查看 Git 倉庫中的提交記錄和歷史修改。 使用 TortoiseGit 查看提交記錄和修改歷史 查看提交記錄&#xff08;Log&#xff09;&#xff1a; 右鍵點擊項目文…

抖音里賣什么最賺錢?4個冷門的高利潤商品,還有誰不知道!

哈嘍~我的電商月月 做抖音小店的新手朋友&#xff0c;一定很想知道&#xff0c;在抖音里賣什么最賺錢&#xff1f; 很多人都會推薦&#xff0c;日常百貨&#xff0c;小風扇&#xff0c;女裝&#xff0c;寵物用品等等&#xff0c;這些商品確實很好做&#xff0c;你們可以試試 …

Euraka詳解:實現微服務架構的關鍵組件

在當今互聯網時代&#xff0c;微服務架構已經成為許多企業構建和部署應用程序的首選方法之一。而要在微服務架構中實現高可用性和靈活性&#xff0c;服務發現和注冊是至關重要的一環。Eureka作為Netflix開源的服務發現組件&#xff0c;為實現這一目標提供了高效可靠的解決方案。…

備忘錄可以統計字數嗎?備忘錄里在哪查看字數?

在這個信息爆炸的時代&#xff0c;很多人喜歡使用備忘錄app來記錄生活中的點點滴滴。備忘錄不僅可以幫助我們記事、安排日程&#xff0c;還能提醒我們完成各種任務&#xff0c;是我們日常生活中不可或缺的小助手。 然而&#xff0c;在使用備忘錄時&#xff0c;有時我們會遇到需…

不用BookStack的企業都在用什么知識庫軟件

現如今&#xff0c;越來越多的企業使用知識庫軟件對企業內部知識進行管理。BookStack作為一款功能強大的開源知識庫軟件&#xff0c;成為很多企業的首選。但是還是有一部分人群認為BookStack不適合他們的企業那么他們都是在用什么別的知識庫軟件呢&#xff1f;LookLook同學今天…

《python本機環境多版本切換》-兩種方式以及具體使用--venv/pyenv+pycharm測試

阿丹&#xff1a; source myenv/bin/activate 在開發使用rasa的時候發現自己安裝的python環境是3.12的&#xff0c;和rasa不兼容&#xff0c;所以實踐一下更換多python環境。 使用虛擬環境 在Python中使用虛擬環境來切換Python版本是一個常見的做法&#xff0c;這可以幫助你…