SaaS多租戶,如何設計?

一、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)自己的軟件需求。


SaaS模式

什么是多租戶技術(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)設計的對比


隔離架構(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容