一、SaaS多租戶簡介
多租戶技術(shù)是一種軟件架構(gòu)技術(shù),它是在探討與實現(xiàn)如何于多用戶的環(huán)境下共用相同的系統(tǒng)或程序組件,并且仍可確保各用戶間數(shù)據(jù)的隔離性。它是為共用的數(shù)據(jù)中心內(nèi)如何以單一系統(tǒng)架構(gòu)與服務提供多數(shù)客戶端相同甚至可定制化的服務,并且仍可保障客戶的數(shù)據(jù)隔離。簡單來說是一個單獨的實例可以為多個組織服務。
多租戶是SaaS(Software-as-a-Service)下的一個概念,意思為軟件及服務,即通過網(wǎng)絡提供軟件服務。SaaS平臺供應商將應用軟件統(tǒng)一部署在自己的服務器上,客戶端可以根據(jù)工作的實際需求,通過互聯(lián)網(wǎng)向廠商租用所需的應用軟件服務,按定購的服務多少和時間長短向廠商支付費用,并通過互聯(lián)網(wǎng)獲得SaaS平臺供應商提供的服務。
SaaS服務尤其利于一些中小企業(yè),以低成本實現(xiàn)自己的軟件需求。

什么是多租戶技術(shù)
多租戶技術(shù)或稱多重租賃技術(shù),是一種軟件架構(gòu)技術(shù),是實現(xiàn)如何在多用戶環(huán)境下(此處的多用戶一般是面向企業(yè))共用相同的系統(tǒng)或程序組件,并且確保各用戶間數(shù)據(jù)隔離性。
在一臺服務器上運行單個應用實例,它為多個租戶(客戶)提供服務。從定義中我們可以理解:多租戶是一種架構(gòu),目的是為了讓多用戶環(huán)境下使用同一套程序,且保證用戶間數(shù)據(jù)隔離。多租戶的重點就是同程序下實現(xiàn)多用戶數(shù)據(jù)的隔離。
1.1什么是SaaS多租戶
SaaS,是Software-as-a-Service的縮寫名稱,意思為軟件即服務,即通過網(wǎng)絡提供軟件服務。
SaaS平臺供應商將應用軟件統(tǒng)一部署在自己的服務器上,客戶可以根據(jù)工作實際需求,通過互聯(lián)網(wǎng)向廠商訂購所需的應用軟件服務,按定購的服務多少和時間長短向廠商支付費用,并通過互聯(lián)網(wǎng)獲得SaaS平臺供應商提供的服務。
SaaS服務通?;谝惶讟藴受浖到y(tǒng)為成百上千的不同客戶(又稱為租戶)提供服務。這要求SaaS服務能夠支持不同租戶之間數(shù)據(jù)和配置的隔離,從而保證每個租戶數(shù)據(jù)的安全與隱私,以及用戶對諸如界面、業(yè)務邏輯、數(shù)據(jù)結(jié)構(gòu)等的個性化需求。由于SaaS同時支持多個租戶,每個租戶又有很多用戶,這對支撐軟件的基礎設施平臺的性能、穩(wěn)定性和擴展性提出很大挑戰(zhàn)。
多租戶是SaaS領(lǐng)域的特有產(chǎn)物,探究何為多租戶需回歸到對SaaS的理解上。
SaaS服務是指部署在云上的,客戶可以按需購買,并通過網(wǎng)絡請求就能獲取到的服務;也就是說,在這樣的場景下,會有N個客戶同時使用同一套SaaS服務。
那么對SaaS服務供應商來說,構(gòu)建SaaS體系需要完成兩部分工作:上層服務+底層多租戶系統(tǒng)。
上層服務是供應商對外售賣的軟件服務,其可以為客戶創(chuàng)造價值、為公司帶來營收;而底層多租戶系統(tǒng)則是SaaS模式實現(xiàn)的具體方式,公司在對外售賣SaaS服務時,需要考慮如何實現(xiàn)客戶之間的數(shù)據(jù)隔離、服務的權(quán)限控制、計費管理等;因此需要引入多租戶概念來解決上述問題。
通過多租戶系統(tǒng),公司可以更好的管理客戶和上層服務,客戶也可以更好的使用軟件服務。這也就是多租戶系統(tǒng)存在的意義了。
1.2 SaaS多租戶的優(yōu)勢
開發(fā)和運維成本低
按需付費,節(jié)約成本
即租即用,軟件版本更新快
故障排查更及時
大數(shù)據(jù)和AI的能力支持更強大
1.3 多租戶模型

如圖所示,涉及主要模型有以下幾類:
(1)租戶:指一個企業(yè)客戶或是個人客戶,租戶之間數(shù)據(jù)與行為隔離,上下級租戶間通過授權(quán)實現(xiàn)數(shù)據(jù)共享。每個租戶只能操作歸屬或授權(quán)給該租戶的數(shù)據(jù);
(2)組織:如果租戶是一個企業(yè)客戶,通常就會擁有自己的組織架構(gòu);
(3)用戶:租戶下的具體使用者,擁有用戶名、密碼、郵箱等賬號信息的自然人;
(4)角色:用戶操作權(quán)限的集合;
(5)員工:組織內(nèi)的某位員工;
(6)解決方案:為了解決客戶的某類型業(yè)務問題,SaaS供應商一般都將產(chǎn)品和服務組合在一起,為客戶提供整體的打包方案;
(7)產(chǎn)品能力:能夠幫助客戶實現(xiàn)場景解決方案閉環(huán)的能力;
(8)資源域:用來運行1個或多個產(chǎn)品應用的一套云資源環(huán)境;
(9)云資源:SaaS產(chǎn)品一般都部署在各種云平臺上,例如阿里云、騰訊云、華為云等。對這些云平臺提供的計算、存儲、網(wǎng)絡、容器等資源,抽象為云資源。
二、SaaS多租戶的數(shù)據(jù)隔離設計方案
多租戶對于用戶來說,最主要的一點就在于數(shù)據(jù)隔離。
絕對不能出現(xiàn):一個用戶登了A用戶單位的號,但是看到了B用戶單位的數(shù)據(jù)。因此,多租戶的數(shù)據(jù)庫設計方案和代碼實現(xiàn)就相當有必要考慮了。
目前開發(fā)者們普遍接受的SaaS多租戶設計方案,常見的大概就3種:即為每個租戶提供獨立的數(shù)據(jù)庫、獨立的表空間、按字段區(qū)分租戶,每種方案都有其各自的適用情況。
一個租戶獨立一個數(shù)據(jù)庫
一個租戶獨立使用一個數(shù)據(jù)庫,那就意味著我們的SaaS系統(tǒng)需要連接多個數(shù)據(jù)庫,這種實現(xiàn)方案其實就和分庫分表架構(gòu)設計是一樣的,好處就是數(shù)據(jù)隔離級別特別高、安全性好,畢竟一個租戶單用一個數(shù)據(jù)庫,但是物理硬件成本,維護成本也變高了。
獨立的表空間
這種方案的實現(xiàn)方式,就是所有租戶共用一個數(shù)據(jù)庫系統(tǒng),但是每個租戶在數(shù)據(jù)庫系統(tǒng)中擁有一個獨立的表空間。
按租戶id字段隔離租戶
這種方案是多租戶方案中最簡單的數(shù)據(jù)隔離方案,即在每張表中都添加一個用于區(qū)分租戶的字段(如tenant_id或org_id啥的)來標識每條數(shù)據(jù)屬于哪個租戶,當進行查詢的時候每條語句都要添加該字段作為過濾條件,其特點是所有租戶的數(shù)據(jù)全都存放在同一個表中,數(shù)據(jù)的隔離性是最低的,完全是通過字段來區(qū)分的,很容易把數(shù)據(jù)搞串或者誤操作。
2.1三種數(shù)據(jù)隔離架構(gòu)設計的對比

大部分公司都是采用第三種多租戶設計方案:按租戶id字段隔離租戶架構(gòu)設計實現(xiàn)多租戶數(shù)據(jù)隔離的。
因為這種方案服務器成本最低,但是提高了開發(fā)成本。
2.2MyBatis-Plus多租戶插件優(yōu)雅實現(xiàn)數(shù)據(jù)隔離
該系統(tǒng)只有一個數(shù)據(jù)庫,所有租戶共用數(shù)據(jù)表。
在每一個數(shù)據(jù)表中增加一列租戶ID,用以區(qū)分租戶的數(shù)據(jù)。
增刪改查時,一定要帶上租戶ID,否則就會操作到其他租戶的數(shù)據(jù)。因此,這里的設計一定要重點考慮。
我們要保證的就是一定不要忘記帶上租戶ID。一個很好的方案就是通過AOP的方案,隱式的為我們的每一個SQL帶上這個租戶ID。
推薦使用MyBatis-Plus來操作數(shù)據(jù)庫的。它提供了插件的機制,我們可以通過攔截它提供的四大組件的某些對象,某些方法,來操作SQL,動態(tài)的為我們的SQL拼接上租戶ID字段。
當然,MyBatis-Plus高版本提供了更加方便的攔截器,并且已經(jīng)將多租戶插件放入JAR包,我們只需稍加實現(xiàn),并將該插件加入到MyBatis的攔截器鏈中,就可以不用再顯示的拼接租戶ID字段了,降低了出錯的概率。
三、MyBatisPlus實現(xiàn)多租戶功能
如果希望以最少的服務器為最多的租戶提供服務,并且租戶接受以犧牲隔離級別換取降低成本??梢圆捎梅桨溉?,即共享數(shù)據(jù)庫,共享數(shù)據(jù)架構(gòu),因為這種方案服務器成本最低,但是提高了開發(fā)成本。
所以MyBatisPlus就提供了一種多租戶的解決方案,實現(xiàn)方式是基于多租戶插件TenantLinelnnerlnterceptor進行實現(xiàn)的。
在MyBatis Plus中,采用“共享數(shù)據(jù)庫,共享數(shù)據(jù)架構(gòu)”方式實現(xiàn)多租戶。
mybatisPlus提供了租戶處理器(Tenantld行級),租戶之間共享數(shù)據(jù)庫,共享數(shù)據(jù)架構(gòu),通過表字段(租戶ID)進行數(shù)據(jù)邏輯隔離。
該種實現(xiàn)方式,需要我們在要實現(xiàn)多租戶的表中添加tenant_id(租戶ID)字段,每次在對數(shù)據(jù)庫操作時都需要在where后面添加租戶判斷條件“tenant_id=用戶的租戶ID”。
然而,使用了MyBatis Plus后,我們就不需要每次都手動在where后面添加tenant_id條件。
注意事項:
多租戶!=權(quán)限過濾,不要亂用,租戶之間時完全隔離的!??!
啟用多租戶后所有執(zhí)行的method的sql都會進行處理。
自寫的sql請按規(guī)范書寫(sql涉及到多個表的每個表都要給別名,特別是inner join的要寫標準的inner join)
<!-- Mybatis-Plus 增強CRUD -->
<dependency>
? ? <groupId>com.baomidou</groupId>
? ? <artifactId>mybatis-plus-boot-starter</artifactId>
? ? <version>3.5.1</version>
</dependency>
<!-- Mybatis-Plus 擴展插件 -->
<dependency>
? ? <groupId>com.baomidou</groupId>
? ? <artifactId>mybatis-plus-extension</artifactId>
? ? <version>3.5.1</version>
</dependency>
TenantLineInnerInterceptor是MybatisPlus中提供的多租戶插件,其使用方法大致分為下面4步:
3.1表及實體類添加租戶ID
應用添加維護一張tenant(租戶表),記錄租戶的信息,每一個租戶,有一個租戶ID。
然后,在需要進行隔離的數(shù)據(jù)表上新增租戶id,例如,現(xiàn)在有數(shù)據(jù)庫表(user)如下:
租戶ID一般用tenant_id

將tenantId用來隔離租戶與租戶之間的數(shù)據(jù),如果要查詢當前服務商的用戶,SQL大致如下:
SELECT * FROM table t WHERE t.tenantId = 1;
3.2application文件中添加多租戶配置和新增配置屬性類
(1)設置環(huán)境變量,配置攔截規(guī)則:
tenant.enable: 可以設置是否開啟多租戶,
tenant.ignoreTables:需要進行租戶id過濾的表名集合。
tenant.filterTables:對多租戶的表設置白名單忽略多租戶攔截等。例如sys_user表結(jié)構(gòu)中,沒有tenant_id多租戶字段,那么多租戶攔截器不攔截該表。
#多租戶配置
tenant:
? enable: true
? column: tenant_id
? filterTables:
? ignoreTables:
? ? - sys_app
? ? - sys_config
? ? - sys_dict_data
? ? - sys_dict_type
? ? - sys_logininfor
? ? - sys_menu
? ? - sys_notice
? ? - sys_oper_log
? ? - sys_role
? ? - sys_role_menu
? ? - sys_user
? ? - sys_user_role
? ignoreLoginNames:
(2)多租戶配置屬性類
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 多租戶配置屬性類
*
* @author hege
* @Date 2023-08-25
*
*/
@Data
@ConfigurationProperties(prefix = "tenant")
public class TenantProperties {
? ? /**
? ? * 是否開啟多租戶
? ? */
? ? private Boolean enable = true;
? ? /**
? ? * 租戶id字段名
? ? */
? ? private String column = "tenant_id";
? ? /**
? ? * 需要進行租戶id過濾的表名集合
? ? */
? ? private List<String> filterTables;
? ? /**
? ? * 需要忽略的多租戶的表,此配置優(yōu)先filterTables,若此配置為空,則啟用filterTables
? ? */
? ? private List<String> ignoreTables;
? ? /**
? ? * 需要排除租戶過濾的登錄用戶名
? ? */
? ? private List<String> ignoreLoginNames;
}
3.3編寫多租戶處理器實現(xiàn)TenantLineHandler接口
在 MyBatis Plus 中,提供了 TenantLineInnerInterceptor 插件和 TenantLineHandler 接口。
其中:
TenantLineInnerInterceptor 插件用來自動向每個 SQL 的 where 后面添加判斷條件“tenant_id=用戶的租戶ID”。
而 TenantLineHandler 接口用來給 TenantLineInnerInterceptor 插件提供租戶ID、租戶字段名。
TenantLineHandler 接口定義如下:
public interface TenantHandler {
? /**
? * 獲取租戶 ID 值表達式,支持多個 ID 條件查詢
? * 支持自定義表達式,比如:tenant_id in (1,2) @since 2019-8-2
? * @param where 參數(shù) true 表示為 where 條件 false 表示為 insert 或者 select 條件
? * @return 租戶 ID 值表達式
? */
? Expression getTenantId(boolean where);
? /**
? * 獲取租戶字段名
? * @return 租戶字段名
? */
? String getTenantIdColumn();
? /**
? * 根據(jù)表名判斷是否進行過濾
? * @param tableName 表名
? * @return 是否進行過濾, true:表示忽略,false:需要解析多租戶字段
? */
? boolean doTableFilter(String tableName);
}
實現(xiàn)TenantHandler接口并實現(xiàn)它的方法,下面是一個例子:
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
/**
* 多租戶處理器實現(xiàn)TenantLineHandler接口
*
* @author hege
* @Date 2023-08-25
*/
public class MultiTenantHandler implements TenantLineHandler {
? ? private final TenantProperties properties;
? ? public MultiTenantHandler(TenantProperties properties) {
? ? ? ? this.properties = properties;
? ? }
? ? /**
? ? * 獲取租戶ID值表達式,只支持單個ID值 (實際應該從用戶信息中獲取)
? ? *
? ? * @return 租戶ID值表達式
? ? */
? ? @Override
? ? public Expression getTenantId() {
? ? ? ? //實際應該從用戶信息中獲取
? ? ? ? if(SecurityUtils.getTenantLoginUser()!=null)
? ? ? ? {
? ? ? ? ? ? //SecurityUtils 從ThreadLocal里面的安全上下文 中獲取 用戶所歸屬的單位id(租戶id)
? ? ? ? ? ? Long tenantId = SecurityUtils.getLoginUser().getUser().getRootPartyId();
? ? ? ? ? ? if(tenantId!=null)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? return new LongValue(tenantId);
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return new LongValue(0);
? ? }
? ? /**
? ? * 獲取租戶字段名,默認字段名叫: tenant_id
? ? *
? ? * @return 租戶字段名
? ? */
? ? @Override
? ? public String getTenantIdColumn() {
? ? ? ? //通過配置獲取
? ? ? ? return properties.getColumn();
? ? }
? ? /**
? ? * 根據(jù)表名判斷是否忽略拼接多租戶條件
? ? *
? ? * 默認都要進行解析并拼接多租戶條件
? ? *
? ? * @param tableName 表名
? ? * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租戶條件
? ? */
? ? @Override
? ? public boolean ignoreTable(String tableName) {
? ? ? ? //忽略指定用戶對租戶的數(shù)據(jù)過濾
? ? ? ? List<String> ignoreLoginNames=properties.getIgnoreLoginNames();
? ? ? ? //SecurityUtils 從ThreadLocal里面的安全上下文 中獲取 用戶名稱
? ? ? ? String loginName=SecurityUtils.getTenantUsername();
? ? ? ? if(null!=ignoreLoginNames && ignoreLoginNames.contains(loginName)){
? ? ? ? ? ? return true;
? ? ? ? }
? ? ? ? //忽略指定表對租戶數(shù)據(jù)的過濾
? ? ? ? List<String> ignoreTables = properties.getIgnoreTables();
? ? ? ? if (null != ignoreTables && ignoreTables.contains(tableName)) {
? ? ? ? ? ? return true;
? ? ? ? }
? ? ? ? return false;
? ? }
}
SecurityUtils 從ThreadLocal里面的安全上下文 中獲取 用戶名稱, 用戶所歸屬的單位id(租戶id)
3.4MybatisPlus配置類啟用多租戶攔截插件運行sql實例:
前面講到,在 MyBatis Plus 中,提供了 TenantLineInnerInterceptor 插件和 TenantLineHandler 接口。
其中,TenantLineInnerInterceptor 插件用來自動向每個 SQL 的 where 后面添加判斷條件“tenant_id=用戶的租戶ID”。
TenantLineInnerInterceptor 插件 調(diào)用 TenantLineHandler 接口用來給 提供租戶ID、租戶字段名。
使用 @Configuration 和 @Bean 注解配置 MyBatis Plus 的多租戶插件,
iimport com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* Mybatis Plus 配置
*
* @author hege
*/
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
@EnableConfigurationProperties(TenantProperties.class)
public class MybatisPlusConfig {
? ? /**
? ? * 如果用了分頁插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
? ? *
? ? * @param tenantProperties
? ? * @return
? ? */
? ? @Bean
? ? public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties tenantProperties) {
? ? ? ? MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
? ? ? ? if (Boolean.TRUE.equals(tenantProperties.getEnable())) {
? ? ? ? ? ? // 啟用多租戶插件攔截
? ? ? ? ? ? interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new MultiTenantHandler(tenantProperties)));
? ? ? ? }
? ? ? ? // 分頁插件
? ? ? ? interceptor.addInnerInterceptor(paginationInnerInterceptor());
? ? ? ? // 樂觀鎖插件
? ? ? ? interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
? ? ? ? // 阻斷插件
? ? ? ? interceptor.addInnerInterceptor(blockAttackInnerInterceptor());
? ? ? ? return interceptor;
? ? }
}
配置好之后,不管是查詢、新增、修改刪除方法,MP都會自動加上租戶ID的標識,測試如下:
@Test
public void select(){
? List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery().eq(User::getAge, 18));
? users.forEach(System.out::println);
}
運行sql實例:
DEBUG==> Preparing: SELECT id, login_name, name, password,
? ? ? email, salt, sex, age, phone, user_type, status,
? ? organization_id, create_time, update_time, version,
? ? tenant_id FROM sys_user
? WHERE sys_user.tenant_id = '001' AND is_delete = '0' AND age = ?
驗證結(jié)果:
針對MybatisPlus提供的API、自定義Mapper中的statement均可正常攔截,會在SQL執(zhí)行增刪改查的時候自動加上tenant_id。
3.5特定SQL語句忽略攔截
如果在程序中,有部分SQL不需要加上租戶ID的表示,需要過濾特定的sql,或者對于一些超級管理員使用的接口,希望跨租戶查詢、免數(shù)據(jù)鑒權(quán)時,無需多租戶攔截。
怎么辦?
可以通過下面幾種方式實現(xiàn)忽略攔截:
方法1:使用MybatisPlus框架自帶的@InterceptorIgnore注解,以用在Mapper類上,也可以用在方法上
方法2:添加超級用戶賬號白名單,在自定義的Handler里進行邏輯判斷,跳過攔截
方法3:添加數(shù)據(jù)表白名單,在自定義的Handler里進行邏輯判斷,跳過攔截
使用MybatisPlus框架自帶的@InterceptorIgnore注解,以用在Mapper類上,也可以用在方法上, 下面是一個例子:
/**
* 使用@InterceptorIgnore注解,忽略多租戶攔截 <br/>
* 注解@InterceptorIgnore可以用在Mapper類上,也可以用在方法上
*
* @param id
* @return
*/
@InterceptorIgnore(tenantLine = "true")
UserOrgVO myFindByIdNoTenant(@Param(value = "id") Long id);
參考文獻
https://mp.weixin.qq.com/s/TR75wnxsXgFZ2ot1dOvX2w
https://mp.weixin.qq.com/s/CVTuEINWHCLue1oB7Yr3ng
https://mp.weixin.qq.com/s/Nl5Oll9GcF6JB8JvIb2YqA
https://zhuanlan.zhihu.com/p/420696556
https://blog.csdn.net/CSDN2497242041/article/details/132525117
原文鏈接:https://blog.csdn.net/qq_45038038/article/details/135575700