相關文章:
因爲某些原因,臨近上線前我們調整了方案,即使用多數據源的方案去對系統進行多租戶改造,這也是《基於 MyBatis 實現多租戶數據隔離的實踐》中與各位夥伴討論的相對好的方案。這樣改造過程平滑,兩種方案(數據合併方案和多數據源方案)的風險、操作難度不在一個數量級。
雖然多數據源方案相對簡單很多,但還是要注意一些問題。這裏將一些問題記錄一下。
(歷史)系統多數據源配置
AbstractRoutingDataSource
其實在 Spring/Spring Boot 中多數據源並不是什麼麻煩的事情,一般項目都是使用的 AbstractRoutingDataSource
進行多數據源控制。但是歷史系統都有一個問題就是“註釋很少、會有一定程度的封裝”,造成很多功能在改造的時候會有難度。
所以這部分在改造的時候需要把握 AbstractRoutingDataSource
的核心方法。其實這個類就是一個模版類。關鍵要注意這個方法:
protected abstract Object determineCurrentLookupKey();
很明顯需要子類去實現。方法名中有“Key”,在 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineTargetDataSource
方法中:
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
//設置默認數據源,根據 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#setDefaultTargetDataSource 設置
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
可以看到,resolvedDataSources
就是根據 determineCurrentLookupKey
方法返回的 Key 去獲取數據源。那麼 resolvedDataSources
是從哪來的呢:
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
可以看到 resolvedDataSources
的數據來自於 targetDataSources
,而 AbstractRoutingDataSource
也給我們提供了 setTargetDataSources
方法:
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
所以我們可以在子類中將數據源初始化好之後設置到 targetDataSources
即可。
關於多數據源的概念和 @MapperScan
多數據源最直白的解釋就是一個數據庫就是一個數據源,但是我這個項目由於歷史設計原因,可能會有理解上的誤導,比如在我這個項目中有一個 DataSourceName 的概念,但是這個 DataSourceName 是跟租戶名稱掛鉤的,即:
再結合相關的配置就成了這樣的對應關係:
要注意的是我們是可以配置多個 MapperScan
的,從而配置多個 basePackages
、sqlSessionFactoryRef
。但是在多租戶的系統中其實 Mapper
只有一套,所以這個對應關係可以改一下,要弱化這裏 DataSourceName 的概念:
即所有租戶共用 MapperScan
等配置,所有的數據源都是同一級的。那麼到底走哪個數據源呢,需要將當前環境設置到 ThreadLocal
中,然後 AbstractRoutingDataSource
再基於當前環境和讀寫分離註解去選擇數據源。
定時任務
定時任務我覺得這塊也沒有設計的很好,後續會再改進。在《基於 MyBatis 實現多租戶數據隔離的實踐》也做了相關介紹,系統代碼中所有的數據已經有租戶標識 region
去數據隔離了。
循環所有租戶,每次循環將
region
租戶標識參數放入當前循環中,租戶過多可以拆分多個定時任務;
這個方案在多數據源方案中是不行的,因爲多數據源最關鍵的是多個庫,也就是說一個定時任務需要跑多個庫,即系統環境設置級別是高於合併數據方案的設置級別的。所以需要循環所有環境,每次循環在 ThreadLocal
中設置環境。
歡迎關注公衆號