實(shí)用:Spring的多租戶數(shù)據(jù)源管理 AbstractRoutingDataSource!
本文轉(zhuǎn)載自微信公眾號(hào)「小姐姐味道」,作者小姐姐養(yǎng)的狗02號(hào) 。轉(zhuǎn)載本文請(qǐng)聯(lián)系小姐姐味道公眾號(hào)。
很多情況,我們確實(shí)需要在一個(gè)服務(wù)中訪問多個(gè)數(shù)據(jù)源。雖然它讓整體設(shè)計(jì)變的不那么優(yōu)雅,但真實(shí)的世界確實(shí)需要它。比如,你的業(yè)務(wù)為兩個(gè)比較大的客戶服務(wù),但你希望他們能夠共用一套代碼。
也就是說,你的代碼剛開始沒有考慮設(shè)計(jì)多租戶這種功能,但后面又有這種蛋疼的需求。但還好不是爆炸式的租戶增長。
除了引入一些分庫分表組件,Spring自身提供了AbstractRoutingDataSource的方式,讓多數(shù)數(shù)據(jù)源的管理成為可能。其實(shí)分庫分表組件使用上限制很多,你不得不首先梳理這座屎山,接下來還要忍受中間件對(duì)你的SQL的苛刻要求;反而是一些野路子,能夠讓代碼的改動(dòng)量盡量的減少。
心動(dòng)不如行動(dòng)。接下來,就讓我們來看一下它的具體實(shí)現(xiàn)吧。
1.基本原理
多數(shù)據(jù)源能進(jìn)行動(dòng)態(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ā)。它就可以手動(dòng)設(shè)置當(dāng)前請(qǐng)求的數(shù)據(jù)庫標(biāo)識(shí),然后路由到正確的庫表里面。
- @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è)屬性一直透?jìng)飨氯ツ?
在Java中,可以使用ThreadLocal綁定這個(gè)透?jìng)鞯膶傩浴O馭pring的嵌套事務(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.問題
通過以上簡(jiǎn)單的代碼,就可以實(shí)現(xiàn)Spring簡(jiǎn)單的多數(shù)據(jù)源管理。但明顯的,它還存在很多問題。
- 需要產(chǎn)品設(shè)計(jì)選擇模式,進(jìn)行業(yè)務(wù)切換。
- 前端可以采用放在localStroage的方式,保存屬性,可使用攔截器方式將變量每次都傳遞。
- 后端每次請(qǐng)求,都需要帶上目標(biāo)db,可以采用放在ThreadLocal里的方式。但ThreadLocal有線程透?jìng)鞯膯栴},如果任務(wù)里開啟了子線程,則變量不能共享。
- 由于表是動(dòng)態(tài)選擇的,所以JPA自動(dòng)創(chuàng)建和update等模式,將不可用。不方便測(cè)試和單元測(cè)試,在測(cè)試接口的時(shí)候,也需要每次強(qiáng)制指定指向的庫。
- 由于是修改數(shù)據(jù)源的模式,每次增加庫,都需要重新啟動(dòng)上線才可以。如果要做到動(dòng)態(tài)性,數(shù)據(jù)源銷毀是個(gè)問題。
End
對(duì)于一個(gè)微服務(wù)來說,有很多默認(rèn)的限制策略,比如,不同域之間的服務(wù)是不能共享一個(gè)數(shù)據(jù)庫的。這些基本原則,把微服務(wù)整的清清爽爽,是一些基本的原則。
同理的,如果我們?cè)谠O(shè)計(jì)開始,就給每一張表加上租戶的字段ID,那么寫代碼的時(shí)候就順暢的多。但是世界上沒有這么多如果。
原則為何而存在?當(dāng)然是為了讓人去打破的。
編程只是工具,反正代碼在自己手里,怎么玩,看需要,也看心情。條條大路通羅馬,曲徑通幽處,風(fēng)光無限好。
作者簡(jiǎn)介:小姐姐味道 (xjjdog),一個(gè)不允許程序員走彎路的公眾號(hào)。聚焦基礎(chǔ)架構(gòu)和Linux。十年架構(gòu),日百億流量,與你探討高并發(fā)世界,給你不一樣的味道。