基于Mybatis-plus實(shí)現(xiàn)多租戶架構(gòu)的全過(guò)程
多租戶(Multi-Tenant)是SaaS中的一個(gè)重要概念,它是一種軟件架構(gòu)技術(shù),在多個(gè)租戶的環(huán)境下,共享同一套系統(tǒng)實(shí)例,并且租戶之間的數(shù)據(jù)具有隔離性,也就是說(shuō)一個(gè)租戶不能去訪問(wèn)其他租戶的數(shù)據(jù)。基于不同的隔離級(jí)別,通常具有下面三種實(shí)現(xiàn)方案:
1、每個(gè)租戶使用獨(dú)立DataBase,隔離級(jí)別高,性能好,但成本大
2、租戶之間共享DataBase,使用獨(dú)立的Schema
3、租戶之間共享Schema,在表上添加租戶字段,共享數(shù)據(jù)程度最高,隔離級(jí)別最低。
Mybatis-plus在第3層隔離級(jí)別上,提供了基于分頁(yè)插件的多租戶的解決方案,我們對(duì)此來(lái)進(jìn)行介紹。在正式開(kāi)始前,首先做好準(zhǔn)備工作創(chuàng)建兩張表,在基礎(chǔ)字段后都添加租戶字段tenant_id:
CREATE TABLE `user` ( `id` bigint(20) NOT NULL, `name` varchar(20) DEFAULT NULL, `phone` varchar(11) DEFAULT NULL, `address` varchar(64) DEFAULT NULL, `tenant_id` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`) ) CREATE TABLE `dept` ( `id` bigint(20) NOT NULL, `dept_name` varchar(64) DEFAULT NULL, `comment` varchar(128) DEFAULT NULL, `tenant_id` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`) )
在項(xiàng)目中導(dǎo)入需要的依賴:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>3.1</version>
</dependency>Mybatis-plus 配置類(lèi):
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
List<ISqlParser> sqlParserList=new ArrayList<>();
TenantSqlParser tenantSqlParser=new TenantSqlParser();
tenantSqlParser.setTenantHandler(new TenantHandler() {
@Override
public Expression getTenantId(boolean select) {
String tenantId = "3";
return new StringValue(tenantId);
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean doTableFilter(String tableName) {
return false;
}
});
sqlParserList.add(tenantSqlParser);
paginationInterceptor.setSqlParserList(sqlParserList);
return paginationInterceptor;
}
}這里主要實(shí)現(xiàn)的功能:
創(chuàng)建SQL解析器集合
創(chuàng)建租戶SQL解析器
設(shè)置租戶處理器,具體處理租戶邏輯
這里暫時(shí)把租戶的id固定寫(xiě)成3,來(lái)進(jìn)行測(cè)試。測(cè)試執(zhí)行全表語(yǔ)句:
public List<User> getUserList() {
return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
}使用插件解析執(zhí)行的SQL語(yǔ)句,可以看到自動(dòng)在查詢條件后加上了租戶過(guò)濾條件:

那么在實(shí)際的項(xiàng)目中,怎么將租戶信息傳給租戶處理器呢,根據(jù)情況我們可以從緩存或者請(qǐng)求頭中獲取,以從Request請(qǐng)求頭獲取為例:
@Override
public Expression getTenantId(boolean select) {
ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String tenantId = request.getHeader("tenantId");
return new StringValue(tenantId);
}前端在發(fā)起http請(qǐng)求時(shí),在Header中加入tenantId字段,后端在處理器中獲取后,設(shè)置為當(dāng)前這次請(qǐng)求的租戶過(guò)濾條件。
如果是基于請(qǐng)求頭攜帶租戶信息的情況,那么在使用中可能會(huì)遇到一個(gè)坑,如果當(dāng)使用多線程的時(shí)候,新開(kāi)啟的異步線程并不會(huì)自動(dòng)攜帶當(dāng)前線程的Request請(qǐng)求。
@Override
public List<User> getUserListByFuture() {
Callable getUser=()-> userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
FutureTask<List<User>> future=new FutureTask<>(getUser);
new Thread(future).start();
try {
return future.get();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}執(zhí)行上面的方法,可以看出是獲取不到當(dāng)前的Request請(qǐng)求的,因此無(wú)法獲得租戶id,會(huì)導(dǎo)致后續(xù)報(bào)錯(cuò)空指針異常:

修改的話也非常簡(jiǎn)單,開(kāi)啟RequestAttributes的子線程共享,修改上面的代碼:
@Override
public List<User> getUserListByFuture() {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
Callable getUser=()-> {
RequestContextHolder.setRequestAttributes(sra, true);
return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
};
FutureTask<List<User>> future=new FutureTask<>(getUser);
new Thread(future).start();
try {
return future.get();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}這樣修改后,在異步線程中也能正常的獲取租戶信息了。
那么,有的小伙伴可能要問(wèn)了,在業(yè)務(wù)中并不是所有的查詢都需要過(guò)濾租戶條件啊,針對(duì)這種情況,有兩種方式來(lái)進(jìn)行處理。
1、如果整張表的所有SQL操作都不需要針對(duì)租戶進(jìn)行操作,那么就對(duì)表進(jìn)行過(guò)濾,修改doTableFilter方法,添加表的名稱:
@Override
public boolean doTableFilter(String tableName) {
List<String> IGNORE_TENANT_TABLES= Arrays.asList("dept");
return IGNORE_TENANT_TABLES.stream().anyMatch(e->e.equalsIgnoreCase(tableName));
}這樣,在dept表的所有查詢都不進(jìn)行過(guò)濾:

2、如果有一些特定的SQL語(yǔ)句不想被執(zhí)行租戶過(guò)濾,可以通過(guò)@SqlParser注解的形式開(kāi)啟,注意注解只能加在Mapper接口的方法上:
@SqlParser(filter = true)
@Select("select * from user where name =#{name}")
User selectUserByName(@Param(value="name") String name);或在分頁(yè)攔截器中指定需要過(guò)濾的方法:
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setSqlParserFilter(metaObject->{
MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
// 對(duì)應(yīng)Mapper、dao中的方法
if("com.cn.tenant.dao.UserMapper.selectUserByPhone".equals(ms.getId())){
return true;
}
return false;
});
...
}上面這兩種方式實(shí)現(xiàn)的功能相同,但是如果需要過(guò)濾的SQL語(yǔ)句很多,那么第二種方式配置起來(lái)會(huì)比較麻煩,因此建議通過(guò)注解的方式進(jìn)行過(guò)濾。
除此之外,還有一個(gè)比較容易踩的坑就是在復(fù)制Bean時(shí),不要復(fù)制租戶id字段,否則會(huì)導(dǎo)致SQL語(yǔ)句報(bào)錯(cuò):
public void createSnapshot(Long userId){
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, userId));
UserSnapshot userSnapshot=new UserSnapshot();
BeanUtil.copyProperties(user,userSnapshot);
userSnapshotMapper.insert(userSnapshot);
}查看報(bào)錯(cuò)可以看出,本身Bean的租戶字段不為空的情況下,SQL又自動(dòng)添加一次租戶查詢條件,因此導(dǎo)致了報(bào)錯(cuò):

我們可以修改復(fù)制Bean語(yǔ)句,手動(dòng)忽略租戶id字段,這里使用的是hutool的BeanUtil工具類(lèi),可以添加忽略字段。
BeanUtil.copyProperties(user,userSnapshot,"tenantId");
在忽略了租戶id的拷貝后,查詢可以正常執(zhí)行。
最后,再來(lái)看一下對(duì)聯(lián)表查詢的支持,首先看一下包含子查詢的SQL:
@Select("select * from user where id in (select id from user_snapshot)")
List<User> selectSnapshot();查看執(zhí)行結(jié)果,可以看見(jiàn),在子查詢的內(nèi)部也自動(dòng)添加的租戶查詢條件:

再來(lái)看一下使用Join進(jìn)行聯(lián)表查詢:
@Select("select u.* from user u left join user_snapshot us on u.id=us.id")
List<User> selectSnapshot();同樣,會(huì)在左右兩張表上都添加租戶的過(guò)濾條件:

再看一下不使用Join的普通聯(lián)表查詢:
@Select("select u.* from user u ,user_snapshot us,dept d where u.id=us.id and d.id is not null")
List<User> selectSnapshot();
查看執(zhí)行結(jié)果,可以看見(jiàn)在這種情況下,只在FROM關(guān)鍵字后面的第一張表上添加了租戶的過(guò)濾條件,因此如果使用這種查詢方式,需要額外注意,用戶需要手動(dòng)在SQL語(yǔ)句中添加租戶過(guò)濾。
總結(jié)
到此這篇關(guān)于基于Mybatis-plus實(shí)現(xiàn)多租戶架構(gòu)的文章就介紹到這了,更多相關(guān)Mybatis-plus實(shí)現(xiàn)多租戶架構(gòu)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MyBatis 動(dòng)態(tài)SQL和緩存機(jī)制實(shí)例詳解
這篇文章主要介紹了MyBatis 動(dòng)態(tài)SQL和緩存機(jī)制實(shí)例詳解,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-09-09
java異常:異常處理--try-catch結(jié)構(gòu)詳解
今天小編就為大家分享一篇關(guān)于Java異常處理之try...catch...finally詳解,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2021-09-09
詳解IDEA2020新建spring項(xiàng)目和c3p0連接池的創(chuàng)建和使用
C3P0是一個(gè)開(kāi)源的JDBC連接池,它實(shí)現(xiàn)了數(shù)據(jù)源和JNDI綁定,本文就使用Spring實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
Java調(diào)用Deepseek實(shí)現(xiàn)項(xiàng)目代碼審查
這篇文章主要為大家詳細(xì)介紹了Java如何調(diào)用Deepseek實(shí)現(xiàn)項(xiàng)目代碼審查功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-02-02
java批量導(dǎo)入Excel數(shù)據(jù)超詳細(xì)實(shí)例
這篇文章主要給大家介紹了關(guān)于java批量導(dǎo)入Excel數(shù)據(jù)的相關(guān)資料,EXCEL導(dǎo)入就是文件導(dǎo)入,操作代碼是一樣的,文中給出了詳細(xì)的代碼示例,需要的朋友可以參考下2023-08-08
springMVC向Controller傳值出現(xiàn)中文亂碼的解決方案
這篇文章主要介紹了springMVC向Controller傳值出現(xiàn)中文亂碼的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02

