在進(jìn)行多租戶架構(gòu)(Multi-tenancy)實(shí)現(xiàn)之前缰猴,先了解一下相關(guān)背景知識(shí)
一产艾、什么是多租戶
多租戶技術(shù)或稱多重租賃技術(shù),簡(jiǎn)稱多租戶滑绒。是一種軟件架構(gòu)技術(shù)闷堡,是實(shí)現(xiàn)如何在多用戶環(huán)境下(此處的多用戶一般是面向企業(yè)用戶)共用相同的系統(tǒng)或程序組件,并且可確保各用戶間數(shù)據(jù)的隔離性蹬挤。
簡(jiǎn)單講:在一臺(tái)服務(wù)器上運(yùn)行單個(gè)應(yīng)用實(shí)例缚窿,它為多個(gè)租戶(客戶)提供服務(wù)。從定義中我們可以理解:多租戶是一種架構(gòu)焰扳,目的是為了讓多用戶環(huán)境下使用同一套程序倦零,且保證用戶間數(shù)據(jù)隔離误续。那么重點(diǎn)就很淺顯易懂了,多租戶的重點(diǎn)就是同一套程序下實(shí)現(xiàn)多用戶數(shù)據(jù)的隔離扫茅。SaaS
應(yīng)用基于此實(shí)現(xiàn)蹋嵌。
二、數(shù)據(jù)隔離有三種方案
- 獨(dú)立數(shù)據(jù)庫(kù):簡(jiǎn)單來(lái)說(shuō)就是一個(gè)租戶使用一個(gè)數(shù)據(jù)庫(kù)葫隙,這種數(shù)據(jù)隔離級(jí)別最高栽烂,安全性最好,但是提高成本恋脚。
- 共享數(shù)據(jù)庫(kù)腺办、隔離數(shù)據(jù)架構(gòu):多租戶使用同一個(gè)數(shù)據(jù)庫(kù),但是每個(gè)租戶對(duì)應(yīng)一個(gè)Schema(數(shù)據(jù)庫(kù)user)糟描。
- 共享數(shù)據(jù)庫(kù)怀喉、共享數(shù)據(jù)架構(gòu):使用同一個(gè)數(shù)據(jù)庫(kù),同一個(gè)Schema船响,但是在表中增加了
租戶ID
的字段躬拢,這種共享數(shù)據(jù)程度最高,隔離級(jí)別最低见间。
這里采用方案三聊闯,即共享數(shù)據(jù)庫(kù),共享數(shù)據(jù)架構(gòu)米诉,因?yàn)檫@種方案服務(wù)器成本最低菱蔬,但是提高了開(kāi)發(fā)成本。
三荒辕、Mybatis-plus實(shí)現(xiàn)多租戶方案
為什么選擇MyBatisPlus汗销?
除了一些系統(tǒng)共用的表以外犹褒,其他租戶相關(guān)的表抵窒,我們都需要在sql不厭其煩的加上AND t.tenant_id = ?
查詢條件,稍不注意就會(huì)導(dǎo)致數(shù)據(jù)越界叠骑,數(shù)據(jù)安全問(wèn)題讓人擔(dān)憂李皇。好在有了MybatisPlus這個(gè)神器,可以極為方便的實(shí)現(xiàn)多租戶SQL解析器宙枷。
Mybatis-plus就提供了一種多租戶的解決方案掉房,實(shí)現(xiàn)方式是基于分頁(yè)插件(攔截器)進(jìn)行實(shí)現(xiàn)的。
3.1 第一步:
在應(yīng)用添加維護(hù)一張sys_tenant(租戶管理表)慰丛,在需要進(jìn)行隔離的數(shù)據(jù)表上新增租戶id;
3.2 第二步:
創(chuàng)建表:
CREATE TABLE `orders_1`.`tenant` (
`id` int(0) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
`expire_date` datetime(0) COMMENT '協(xié)議到期時(shí)間',
`amount` decimal(8, 2) COMMENT '金額',
`tenant_id` int(0) COMMENT '租戶ID',
PRIMARY KEY (`id`)
);
自定義系統(tǒng)的上下文,存儲(chǔ)從cookie等方式獲取的租戶ID揽祥,在后續(xù)的getTenantId()使用卓鹿。
package com.erbadagang.mybatis.plus.tenant.config;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @description 系統(tǒng)的上下文幫助類粥烁。ConcurrentHashMap設(shè)置租戶ID,供后續(xù)的MP的getTenantId()取出
* @ClassName: ApiContext
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/12 21:50
* @Copyright:
*/
@Component
public class ApiContext {
private static final String KEY_CURRENT_TENANT_ID = "KEY_CURRENT_TENANT_ID";
private static final Map<String, Object> mContext = new ConcurrentHashMap<>();
public void setCurrentTenantId(Long providerId) {
mContext.put(KEY_CURRENT_TENANT_ID, providerId);
}
public Long getCurrentTenantId() {
return (Long) mContext.get(KEY_CURRENT_TENANT_ID);
}
}
核心類——MyBatisPlusConfig
通過(guò)分頁(yè)插件配置MP多租戶蝇棉。
package com.erbadagang.mybatis.plus.tenant.config;
import com.baomidou.mybatisplus.core.parser.ISqlParser;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* @description MyBatisPlus配置類讨阻,分頁(yè)插件,多租戶也是使用的分頁(yè)插件進(jìn)行的配置篡殷。
* @ClassName: MyBatisPlusConfig
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/12 21:34
* @Copyright:
*/
@Configuration
@MapperScan("com.erbadagang.mybatis.plus.tenant.mapper")//配置掃描的mapper包
public class MyBatisPlusConfig {
@Autowired
private ApiContext apiContext;
/**
* 分頁(yè)插件
*
* @return
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 創(chuàng)建SQL解析器集合
List<ISqlParser> sqlParserList = new ArrayList<>();
// 創(chuàng)建租戶SQL解析器
TenantSqlParser tenantSqlParser = new TenantSqlParser();
// 設(shè)置租戶處理器
tenantSqlParser.setTenantHandler(new TenantHandler() {
// 設(shè)置當(dāng)前租戶ID钝吮,實(shí)際情況你可以從cookie、或者緩存中拿都行
@Override
public Expression getTenantId(boolean select) {
// 從當(dāng)前系統(tǒng)上下文中取出當(dāng)前請(qǐng)求的服務(wù)商ID板辽,通過(guò)解析器注入到SQL中奇瘦。
Long currentProviderId = apiContext.getCurrentTenantId();
if (null == currentProviderId) {
throw new RuntimeException("Get CurrentProviderId error.");
}
return new LongValue(currentProviderId);
}
@Override
public String getTenantIdColumn() {
// 對(duì)應(yīng)數(shù)據(jù)庫(kù)中租戶ID的列名
return "tenant_id";
}
@Override
public boolean doTableFilter(String tableName) {
// 是否需要需要過(guò)濾某一張表
/* List<String> tableNameList = Arrays.asList("sys_user");
if (tableNameList.contains(tableName)){
return true;
}*/
return false;
}
});
sqlParserList.add(tenantSqlParser);
paginationInterceptor.setSqlParserList(sqlParserList);
return paginationInterceptor;
}
}
四、測(cè)試
配置好之后劲弦,不管是查詢链患、新增、修改刪除方法瓶您,MP都會(huì)自動(dòng)加上租戶ID的標(biāo)識(shí)麻捻,測(cè)試如下:
package com.erbadagang.mybatis.plus.tenant;
import com.erbadagang.mybatis.plus.tenant.config.ApiContext;
import com.erbadagang.mybatis.plus.tenant.entity.Tenant;
import com.erbadagang.mybatis.plus.tenant.mapper.TenantMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
/**
* @description 多租戶測(cè)試用例
* @ClassName: MultiTanentApplicationTests
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/12 22:06
* @Copyright:
*/
@SpringBootTest
class MultiTanentApplicationTests {
@Autowired
private ApiContext apiContext;
@Autowired
private TenantMapper tenantMapper;
@Test
public void before() {
// 在上下文中設(shè)置當(dāng)前服務(wù)商的ID
apiContext.setCurrentTenantId(1L);
}
@Test
public void select() {
List<Tenant> tenants = tenantMapper.selectList(null);
tenants.forEach(System.out::println);
}
}
輸出的SQL自動(dòng)包括WHERE tenant_id = 1
:
==> Preparing: SELECT id, expire_date, amount, tenant_id FROM t_tenant WHERE tenant_id = 1
==> Parameters:
<== Total: 0
五、特定SQL過(guò)濾
如果在程序中呀袱,有部分SQL不需要加上租戶ID的表示贸毕,需要過(guò)濾特定的sql,可以通過(guò)如下兩種方式:
5.1 方式一:
在配置分頁(yè)插件中加上配置ISqlParserFilter解析器夜赵,如果配置SQL很多明棍,比較麻煩,不建議寇僧。
//有部分SQL不需要加上租戶ID的表示摊腋,需要過(guò)濾特定的sql。如果比較多不建議這里配置嘁傀。
/*paginationInterceptor.setSqlParserFilter(new ISqlParserFilter() {
@Override
public boolean doFilter(MetaObject metaObject) {
MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
// 對(duì)應(yīng)Mapper或者dao中的方法
if("com.erbadagang.mybatis.plus.tenant.mapper.UserMapper.selectList".equals(ms.getId())){
return true;
}
return false;
}
});*/
5.2 方式二:
通過(guò)租戶注解的形式兴蒸,目前只能作用于Mapper的方法上。特定sql過(guò)濾 過(guò)濾特定的方法 也可以在userMapper需要排除的方法上加入注解SqlParser(filter=true) 排除 SQL 解析细办。
package com.erbadagang.mybatis.plus.tenant.mapper;
import com.baomidou.mybatisplus.annotation.SqlParser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.erbadagang.mybatis.plus.tenant.entity.Tenant;
import org.apache.ibatis.annotations.Select;
/**
* <p>
* Mapper 接口
* </p>
*
* @author 郭秀志 jbcode@126.com
* @since 2020-07-12
*/
public interface TenantMapper extends BaseMapper<Tenant> {
/**
* 自定Wrapper, @SqlParser(filter = true)注解代表不進(jìn)行SQL解析也就沒(méi)有租戶的附加條件橙凳。
*
* @return
*/
@SqlParser(filter = true)
@Select("SELECT count(5) FROM t_tenant ")
public Integer myCount();
}
測(cè)試
@Test
public void myCount() {
Integer count = tenantMapper.myCount();
System.out.println(count);
}
SQL輸出
==> Preparing: SELECT count(5) FROM t_tenant
==> Parameters:
<== Columns: count(5)
<== Row: 0
<== Total: 1
開(kāi)啟 SQL 解析緩存注解生效,如果你的MP版本在3.1.1及以上則不需要配置
# 開(kāi)啟 SQL 解析緩存注解生效笑撞,如果你的MP版本在3.1.1及以上則不需要配置
mybatis-plus:
global-config:
sql-parser-cache: true
底線
本文源代碼使用 Apache License 2.0開(kāi)源許可協(xié)議岛啸,可從Gitee代碼地址通過(guò)git clone
命令下載到本地或者通過(guò)瀏覽器方式查看源代碼。