???????Spring多租戶數(shù)據(jù)源管理 AbstractRoutingDataSource
前言:
很多情況,我們確實(shí)需要在一個(gè)服務(wù)中訪問多個(gè)數(shù)據(jù)源。雖然它讓整體設(shè)計(jì)變的不那么優(yōu)雅,但真實(shí)的世界確實(shí)需要它。比如,你的業(yè)務(wù)為兩個(gè)比較大的客戶服務(wù),但你希望他們能夠共用一套代碼。
也就是說,你的代碼剛開始沒有考慮設(shè)計(jì)多租戶這種功能,但后面又有這種蛋疼的需求。但還好不是爆炸式的租戶增長。
除了引入一些分庫分表組件,Spring自身提供了AbstractRoutingDataSource的方式,讓多數(shù)數(shù)據(jù)源的管理成為可能。其實(shí)分庫分表組件使用上限制很多,你不得不首先梳理這座屎山,接下來還要忍受中間件對你的SQL的苛刻要求;反而是一些野路子,能夠讓代碼的改動量盡量的減少。
心動不如行動。接下來,就讓我們來看一下它的具體實(shí)現(xiàn)吧。
1.基本原理
多數(shù)據(jù)源能進(jìn)行動態(tài)切換的核心就是spring底層提供了AbstractRoutingDataSource類進(jìn)行數(shù)據(jù)源路由。AbstractRoutingDataSource實(shí)現(xiàn)了DataSource接口,所以我們可以將其直接注入到DataSource的屬性上。
我們主要繼承這個(gè)類,實(shí)現(xiàn)里面的方法determineCurrentLookupKey(),而此方法只需要返回一個(gè)數(shù)據(jù)庫的名稱即可。
比如,Controller通過拿到前端業(yè)務(wù)傳遞的數(shù)值,進(jìn)行業(yè)務(wù)邏輯分發(fā)。它就可以手動設(shè)置當(dāng)前請求的數(shù)據(jù)庫標(biāo)識,然后路由到正確的庫表里面。
@Controller public?class?ARDTestController?{ ????@GetMapping("test") ????public?void?chifeng(){ ????????//db-a?應(yīng)該是上層傳遞下來的屬性,我們可以把它放在ThreadLocal里 ????????DataSourceContextHolder.setDbKey("db-a"); ????} }
那么當(dāng)sql語句執(zhí)行的時(shí)候,它如何知道自己需要切換到哪個(gè)數(shù)據(jù)源呢?是不是需要把db-a這個(gè)屬性一直透傳下去呢?
在Java中,可以使用ThreadLocal綁定這個(gè)透傳的屬性。像Spring的嵌套事務(wù)等實(shí)現(xiàn)的原理,也是基于ThreadLocal去運(yùn)行的。所以,DataSourceContextHolder.本質(zhì)上是一個(gè)操作ThreadLocal的類。
public?class?DataSourceContextHolder?{ ????private?static?InheritableThreadLocal<String>?dbKey?=?new?InheritableThreadLocal<>(); ????public?static?void?setDbKey(String?key){ ????????dbKey.set(key); ????} ????public?static?String?getDbKey(){ ????????return?dbKey.get(); ????} }
2.配置代碼
首先,我們自定義了配置文件的格式。如下面的代碼,就配置了db-a和db-b兩個(gè)數(shù)據(jù)庫。
multi: ??dbs: ????db-a: ??????driver-class-name:?org.h2.Driver ??????url:?jdbc:h2:mem:dba;MODE=MYSQL;DATABASE_TO_UPPER=false; ????db-b: ??????driver-class-name:?org.h2.Driver ??????url:?jdbc:h2:mem:dbb;MODE=MYSQL;DATABASE_TO_UPPER=false;
然后,我們將它解析稱properties。
@ConfigurationProperties(prefix?=?"multi") @Configuration public?class?DbsProperties?{ ????private?Map<String,?Map<String,?String>>?dbs?=?new?HashMap<>(); ????public?Map<String,?Map<String,?String>>?getDbs()?{ ????????return?dbs; ????} ????public?void?setDbs(Map<String,?Map<String,?String>>?dbs)?{ ????????this.dbs?=?dbs; ????} }
接下來一步,需要配置整個(gè)應(yīng)用所默認(rèn)的數(shù)據(jù)源。如你所見,它的主要邏輯,就是在運(yùn)行的時(shí)候,從ThreadLocal里取出提前設(shè)置的這個(gè)值。
public?class?DynamicDataSource?extends?AbstractRoutingDataSource?{ ????@Override ????protected?Object?determineCurrentLookupKey()?{ ????????return?DataSourceContextHolder.getDbKey(); ????} }
最后一步,設(shè)置整個(gè)項(xiàng)目中默認(rèn)的DataSource。注意,我們生成DynamicDataSource之后,還需要提供targetDataSource和defaultTargetDataSource兩個(gè)屬性的值,才能夠正常運(yùn)行。
@Configuration public?class?DynamicDataSourceConfiguration?{ ????@Autowired ????DbsProperties?properties; ????@Bean ????public?DataSource?dataSource(){ ????????DynamicDataSource?dataSource?=?new?DynamicDataSource(); ????????final?Map<Object,Object>?targetDataSource??=?getTargetDataSource(); ????????dataSource.setTargetDataSources(targetDataSource); ????????//TODO?默認(rèn)數(shù)據(jù)庫需要設(shè)置 ????????dataSource.setDefaultTargetDataSource(targetDataSource.values().iterator().next()); ????????return?dataSource; ????} ????private?Map<Object,Object>?getTargetDataSource(){ ????????Map<Object,Object>?dataSources?=?new?HashMap<>(); ????????this.properties.getDbs().entrySet().stream() ????????????????.forEach(e->{ ????????????????????DriverManagerDataSource?dmd?=?new?DriverManagerDataSource(); ????????????????????dmd.setUrl(e.getValue().get("url")); ????????????????????dmd.setDriverClassName(e.getValue().get("driver-class-name")); ????????????????????dataSources.put(e.getKey(),dmd); ????????????????}); ????????return??dataSources; ????} }
3.問題
通過以上簡單的代碼,就可以實(shí)現(xiàn)Spring簡單的多數(shù)據(jù)源管理。但明顯的,它還存在很多問題。
- 需要產(chǎn)品設(shè)計(jì)選擇模式,進(jìn)行業(yè)務(wù)切換。
- 前端可以采用放在localStroage的方式,保存屬性,可使用攔截器方式將變量每次都傳遞。
- 后端每次請求,都需要帶上目標(biāo)db,可以采用放在ThreadLocal里的方式。但ThreadLocal有線程透傳的問題,如果任務(wù)里開啟了子線程,則變量不能共享。
- 由于表是動態(tài)選擇的,所以JPA自動創(chuàng)建和update等模式,將不可用。不方便測試和單元測試,在測試接口的時(shí)候,也需要每次強(qiáng)制指定指向的庫。
- 由于是修改數(shù)據(jù)源的模式,每次增加庫,都需要重新啟動上線才可以。如果要做到動態(tài)性,數(shù)據(jù)源銷毀是個(gè)問題。
總結(jié)
對于一個(gè)微服務(wù)來說,有很多默認(rèn)的限制策略,比如,不同域之間的服務(wù)是不能共享一個(gè)數(shù)據(jù)庫的。這些基本原則,把微服務(wù)整的清清爽爽,是一些基本的原則。
同理的,如果我們在設(shè)計(jì)開始,就給每一張表加上租戶的字段ID,那么寫代碼的時(shí)候就順暢的多。但是世界上沒有這么多如果。
原則為何而存在?當(dāng)然是為了讓人去打破的。
編程只是工具,反正代碼在自己手里,怎么玩,看需要,也看心情。條條大路通羅馬,曲徑通幽處,風(fēng)光無限好。
到此這篇關(guān)于Spring多租戶數(shù)據(jù)源管理 AbstractRoutingDataSource的文章就介紹到這了,更多相關(guān)AbstractRoutingDataSource內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring AbstractRoutingDatasource 動態(tài)數(shù)據(jù)源的實(shí)例講解
- 淺談利用Spring的AbstractRoutingDataSource解決多數(shù)據(jù)源的問題
- 詳解利用Spring的AbstractRoutingDataSource解決多數(shù)據(jù)源的問題
- Spring(AbstractRoutingDataSource)實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換示例
- 使用Spring的AbstractRoutingDataSource實(shí)現(xiàn)多數(shù)據(jù)源切換示例
相關(guān)文章
Spring Boot中的WebSocketMessageBrokerConfigurer接口使用
在SpringBoot中,我們可以使用 WebSocketMessageBrokerConfigurer接口來配置WebSocket消息代理,以實(shí)現(xiàn)實(shí)時(shí)通信,具有一定的參考價(jià)值,感興趣的可以了解一下2023-11-11詳解Lombok安裝及Spring Boot集成Lombok
這篇文章主要介紹了詳解Lombok安裝及Spring Boot集成Lombok,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-03-03SpringBoot一個(gè)接口多個(gè)實(shí)現(xiàn)類的調(diào)用方式總結(jié)
這篇文章主要介紹了SpringBoot一個(gè)接口多個(gè)實(shí)現(xiàn)類的調(diào)用方式,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-01-01java僅用30行代碼就實(shí)現(xiàn)了視頻轉(zhuǎn)音頻的批量轉(zhuǎn)換
這篇文章主要介紹了java僅用30行代碼就實(shí)現(xiàn)了視頻轉(zhuǎn)音頻的批量轉(zhuǎn)換,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Spring5源碼解析之Spring中的異步和計(jì)劃任務(wù)
本篇文章主要介紹了Spring5源碼解析之Spring中的異步和計(jì)劃任務(wù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-10-10Eclipse項(xiàng)目怎么導(dǎo)入IDEA并運(yùn)行(超詳細(xì))
這篇文章主要介紹了Eclipse項(xiàng)目怎么導(dǎo)入IDEA并運(yùn)行(超詳細(xì)),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10