【解決方案】多租戶技術架構設計入門(二)
前言
對于整個多租戶技術架構的設計而言,筆者認為最關鍵的就是 3 點:底層數據隔離模式(策略) + 統一的用戶&權限體系(認證鑒權) + 業務層調用時的行為隔離(請求攔截)。
其次可以拓展的有:租戶管理系統 + 門戶系統 + 角色配置中心等。基本的一些概念我在上篇文章中已經有過較為詳細的介紹,此處便不再贅述了。
作為入門系列的第二篇,本文主要分享的是在業務系統的應用內部如何對多數據源進行切換,而底層的數據庫硬件資源管理這部分會簡單帶過(一般由運維團隊來負責搭建)。
下面我就從多數據源設計、技術選型、應用配置、具體實現這幾個方面來做一個詳細的分享。
一、多數據源設計
1.1概念模型
首先我們要先明確:所有接進來的租戶,都是使用同一套代碼,即同一套服務,但每個租戶會擁有屬于自己的數據庫。
本小節先介紹概念模型,數據隔離模式的分析會在1.2小節展開。
在單租戶的時候,每個系統只為一個客戶服務,我們只需要在每個業務系統的配置文件上寫一個數據庫連接,就可以確保該系統的數據會進到這個對應的庫表里。
在多租戶的背景下,這里所有業務系統也都只有一個連接,即一個多數據源庫,根據系統所在的不同環境和租戶連接不同的庫。
這個庫里面只有一張表,每一行數據里放的是所有業務系統各自的數據庫連接,這個設計是不同的系統找到各自庫Url連接的第一步。
下面對幾個關鍵的字段進行解讀:
- system_code:每個業務系統的標識,要求唯一
- data_code:其實就是數據源的標識,一般使用租戶編碼作為標識
- data_name:系統的中文名稱,更有助于區別是哪個系統
- data_url:每個系統對應的數據庫連接 url 地址
- data_env:所屬的運行環境,可以分為 dev、test 和 prod 這3種
怎么樣才能讓每個系統找到屬于自己的庫呢?請看本文的第二、三、四這3個小節。
1.2隔離模式分析
結論先行:本文采用的是共享數據庫實例獨立數據架構的隔離模式。即:所有業務系統的數據都在一個數據庫實例集群中,但是一個數據庫實例里面可以有很多個數據庫,且可以根據租戶對每個數據庫做權限組控制。原因主要有以下幾點:
- 數據量的要求:租戶多、系統多、用戶量大
- 隔離度的要求:要求較高,行業的特殊性會對數據安全比較敏感
- 業務的復雜度:關聯的系統多達上百個,上下游的數據交互十分頻繁
- 成本的考慮:成本雖可以負擔,但既要滿足上面幾點要求,又不能太貴
- 便于計量計費:有了各自的數據庫,方便對客戶做計量計費的統計
數據庫實例集群的規格要高、性能要強,目前主流云廠商如阿里云和華為云等,都有自己 MySQL for RDS 產品,基本可以完美解決數據隔離和數據庫角色權限的需求。
二、技術選型
結論先行:選擇 baomidou(對,Mybatis Plus 就是他們的杰作) 下的 Dynamic Datasource 動態數據庫方案,引入 3 個依賴:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-creator</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.2.0</version>
</dependency>
Maven 的中央倉庫:https://mvnrepository.com/ 搜索關鍵詞,如下圖所示:
下面介紹幾個核心的類以及 api:
//從請求頭中獲取當前租戶編碼
String tenantCode = request.getHeader("Tenantcode");
//切換多數據源的核心工具類,此處將租戶編碼作為數據源key
DynamicDataSourceContextHolder.push(tenantCode);
public DynamicRoutingDataSource dataSource() {
//根據特定的規則選擇要使用的數據源標識(如數據庫名稱、租戶編碼等),根據路由規則,每個數據訪問操作將使用相應的數據源
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(Collections.emptyList());
DataSourceProperty dataSourceProperty = new DataSourceProperty();
//配置文件的 driver-class-name 驅動名
dataSourceProperty.setDriverClassName(this.dataSourceProperties.getDriverClassName());
//數據庫連接 url
dataSourceProperty.setUrl(this.dataSourceProperties.getUrl());
//連接數據庫的用戶名/密碼
dataSourceProperty.setUsername(this.dataSourceProperties.getUsername());
dataSourceProperty.setPassword(this.dataSourceProperties.getPassword());
//創建多數據源連接,即所有的數據源都可以獲取到
DataSource ds = dataSourceCreator.createDataSource(dataSourceProperty);
dynamicRoutingDataSource.addDataSource(this.dynamicDataSourceProperties.getPrimary(), ds);
return dynamicRoutingDataSource;
}
三、應用配置
相較于 Spring 的各種 xml 配置,Spring boot 引入的約定大于配置的這一重大升級,是多數據源切換的重要基礎。
之前單租戶的時候,無論是分布式的單體還是微服務,應用的 application.yml 或者 application.properties 都有一個本系統的數據庫連接。外部發起請求或者被別的系統調用時,本系統產生的數據都會根據這個配置文件里的數據庫連接去進行增刪改查。具體如下:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://host:port/本系統的數據庫名稱
username: 本系統數據庫賬號
password: 本系統數據庫密碼
那么,在多租戶下,是不是有多少個租戶就要在 applicationyml 里寫多少個數據庫連接呢?
答案當然是否定的。
基于第一章選擇的數據隔離模式,顯然將每個租戶的數據庫連接都維護在一個地方是最方便的,于是便有了第一章的多數據源庫。
所以,基于多租戶的業務系統的 applicationyml 里該怎么寫數據庫連接呢?可以這樣寫:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://host:port/多數據源庫名稱
username: 多數據源庫賬號
password: 多數據源庫密碼
initial:
saas:
system-code: springboot-initial ##這是系統的唯一標識,很關鍵
這樣就可以根據租戶標識(data_code)與系統標識(system_code)來唯一確定屬于本系統的數據庫了,具體怎么做,下一節會給出 demo。
四、具體實現
牢牢把握這 4 點:請求攔截 + 租戶編碼 + 本地線程 + 切換數據源。這4點貫穿了整個多租戶數據源切換的全過程,是數據源切換策略的核心。
由于篇幅,以下只演示核心的步驟:
-
攔截器+租戶編碼
public class TenantInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { boolean predHandle = super.preHandle(request, response, handler); if (!CorsUtils.isPreFlightRequest(request)) { String tenantHeader = request.getHeader("Tenantcode"); if (StringUtils.isBlank(tenantHeader)) { throw new RuntimeException("請求錯誤"); } //本地線程設置值 ThreadLocalUtils.setValue(tenantCode); //切換多數據源的核心類,此處將租戶編碼作為數據源 key DynamicDataSourceContextHolder.push(tenantCode); } return predHandle; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { super.afterCompletion(request, response, handler, ex); //請求完成后清除 ThreadLocalUtils.removeValue(); //同樣是清除本次調用線程中的數據源 key DynamicDataSourceContextHolder.clear(); } } -
本地線程
public class ThreadLocalUtils { /** * 不熟悉的同學可以再去復習一下 ThreadLocal 的相關知識 */ private static final ThreadLocal<String> THREADLOCAL = new ThreadLocal<>(); public static void setValue(String value) { THREADLOCAL.set(value); } public static String getValue() { return THREADLOCAL.get(); } public static void removeValue() { THREADLOCAL.remove(); } } -
切換數據源
這里其實就是第一節中那張多數據源庫表的具體實現,實現類還 implements 了 InitializingBean 所以會有 afterPropertiesSet() 方法。
@Override public void afterPropertiesSet() { LambdaQueryWrapper<DynamicTenantDatasource> wrapper = new LambdaQueryWrapper<>(); RunTimeEnv env = RunEnv.searchRunEnv(Collections.singletonList(this.environment.getActiveProfiles())); log.info("當前數據源所處環境:{}", env); assert env != null; wrapper.eq(DynamicTenantDatasource::getRunTimeEnv, env.getValue()) .eq(DynamicTenantDatasource::getSystemCode, this.dynamicProperties.getSystemCode()); this.list(wrapper).forEach(val -> { DataSourceProperty dataSourceProperty = new DataSourceProperty(); //下面是數據源配置 dataSourceProperty.setDriverClassName(val.getDriverClassName()); dataSourceProperty.setUrl(val.getUrl()); dataSourceProperty.setUsername(val.getUserName()); dataSourceProperty.setPassword(val.getPassword()); DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty); //這里就會拿到當前系統的所有租戶編碼了 this.dynamicRoutingDataSource.addDataSource(val.getDataCode(), dataSource); }); }
由于在請求經過攔截器的時候,當前線程已經獲取了當前的租戶編碼,且已經將這個租戶編碼push到了多數據源工具類,那么只要本次請求涉及到數據庫操作,就能唯一確定數據源了,即能唯一確定本次數據會連接到具體哪個庫。
五、文章小結
如果你也對基于多租戶的動態數據源切換有過思考,那么希望我們的思維能迸出一些火花。
作為整個多租戶的數據隔離模式的重要部分,本篇文章盡可能地將筆者的思考由淺到深與大家分享。為了實現整個數據隔離模式的落地,需要大量的實踐來論證可行性,并且需要相當的資源投入才能真正作為成熟的框架部署到生產環境。
其中就少不了運維團隊以及云原生團隊的支持,基于K8s容器的服務治理、鏡像打包、持續的 CI/CD(GitLab+Jenkins)以及整個 DevOps 平臺的搭建,才能讓這套方案和架構發揮最大的作用。
接下來請期待本系列文章的續作,文章如有不足和錯誤,還請大家指正。或者你有其它想說的,也歡迎大家在評論區交流!

對于整個多租戶技術架構的設計而言,筆者認為最關鍵的就是 3 點:數據隔離模式(策略) + 統一的用戶&權限體系 + 調用時的行為隔離(請求攔截)。作為入門系列的第二篇,本文主要分享的是**在業務系統的應用內部如何對多數據源進行切換**,而底層的數據庫硬件資源管理這部分會簡單帶過(一般由運維團隊來負責搭建)。
浙公網安備 33010602011771號