基于Mybatis-plus实现多租户架构的全过程
作者:码农参上 发布时间:2023-08-03 02:10:57
多租户(Multi-Tenant)是SaaS中的一个重要概念,它是一种软件架构技术,在多个租户的环境下,共享同一套系统实例,并且租户之间的数据具有隔离性,也就是说一个租户不能去访问其他租户的数据。基于不同的隔离级别,通常具有下面三种实现方案:
1、每个租户使用独立DataBase,隔离级别高,性能好,但成本大
2、租户之间共享DataBase,使用独立的Schema
3、租户之间共享Schema,在表上添加租户字段,共享数据程度最高,隔离级别最低。
Mybatis-plus在第3层隔离级别上,提供了基于分页插件的多租户的解决方案,我们对此来进行介绍。在正式开始前,首先做好准备工作创建两张表,在基础字段后都添加租户字段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`)
)
在项目中导入需要的依赖:
<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 配置类:
@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;
}
}
这里主要实现的功能:
创建SQL解析器集合
创建租户SQL解析器
设置租户处理器,具体处理租户逻辑
这里暂时把租户的id固定写成3,来进行测试。测试执行全表语句:
public List<User> getUserList() {
return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
}
使用插件解析执行的SQL语句,可以看到自动在查询条件后加上了租户过滤条件:
那么在实际的项目中,怎么将租户信息传给租户处理器呢,根据情况我们可以从缓存或者请求头中获取,以从Request请求头获取为例:
@Override
public Expression getTenantId(boolean select) {
ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String tenantId = request.getHeader("tenantId");
return new StringValue(tenantId);
}
前端在发起http请求时,在Header中加入tenantId字段,后端在处理器中获取后,设置为当前这次请求的租户过滤条件。
如果是基于请求头携带租户信息的情况,那么在使用中可能会遇到一个坑,如果当使用多线程的时候,新开启的异步线程并不会自动携带当前线程的Request请求。
@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;
}
执行上面的方法,可以看出是获取不到当前的Request请求的,因此无法获得租户id,会导致后续报错空指针异常:
修改的话也非常简单,开启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;
}
这样修改后,在异步线程中也能正常的获取租户信息了。
那么,有的小伙伴可能要问了,在业务中并不是所有的查询都需要过滤租户条件啊,针对这种情况,有两种方式来进行处理。
1、如果整张表的所有SQL操作都不需要针对租户进行操作,那么就对表进行过滤,修改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表的所有查询都不进行过滤:
2、如果有一些特定的SQL语句不想被执行租户过滤,可以通过@SqlParser注解的形式开启,注意注解只能加在Mapper接口的方法上:
@SqlParser(filter = true)
@Select("select * from user where name =#{name}")
User selectUserByName(@Param(value="name") String name);
或在分页 * 中指定需要过滤的方法:
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setSqlParserFilter(metaObject->{
MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
// 对应Mapper、dao中的方法
if("com.cn.tenant.dao.UserMapper.selectUserByPhone".equals(ms.getId())){
return true;
}
return false;
});
...
}
上面这两种方式实现的功能相同,但是如果需要过滤的SQL语句很多,那么第二种方式配置起来会比较麻烦,因此建议通过注解的方式进行过滤。
除此之外,还有一个比较容易踩的坑就是在复制Bean时,不要复制租户id字段,否则会导致SQL语句报错:
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);
}
查看报错可以看出,本身Bean的租户字段不为空的情况下,SQL又自动添加一次租户查询条件,因此导致了报错:
我们可以修改复制Bean语句,手动忽略租户id字段,这里使用的是hutool的BeanUtil工具类,可以添加忽略字段。
BeanUtil.copyProperties(user,userSnapshot,"tenantId");
在忽略了租户id的拷贝后,查询可以正常执行。
最后,再来看一下对联表查询的支持,首先看一下包含子查询的SQL:
@Select("select * from user where id in (select id from user_snapshot)")
List<User> selectSnapshot();
查看执行结果,可以看见,在子查询的内部也自动添加的租户查询条件:
再来看一下使用Join进行联表查询:
@Select("select u.* from user u left join user_snapshot us on u.id=us.id")
List<User> selectSnapshot();
同样,会在左右两张表上都添加租户的过滤条件:
再看一下不使用Join的普通联表查询:
@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();
查看执行结果,可以看见在这种情况下,只在FROM关键字后面的第一张表上添加了租户的过滤条件,因此如果使用这种查询方式,需要额外注意,用户需要手动在SQL语句中添加租户过滤。
来源:https://juejin.cn/post/6966770686652219406


猜你喜欢
- 本文实例为大家分享了java实现购物车功能的具体代码,供大家参考,具体内容如下1 需要实现1、实现淘淘商城的购物车功能2 购物车功能2.1
- 前言java8借鉴了第三方日期库joda很多的优点java.time包类名描述Instant时间戳Duration持续时间,时间差Local
- 一、安装及配置Genymotion(1)由于Eclipse中自带的SDK模拟器,启动之慢,不说了 现在给大家介绍一种比较快的模拟器Genym
- 一、前言 高斯混合模型(Gaussian Mixture Model)简称GMM,是一种业界广泛使用的聚类算法。它是多个高斯分布函
- Java 编程语言中时间的处理类有 Date类与 Calendar类。目前官方不推荐使用 Date类,因为其不利于国际化;而是推荐使用 Ca
- AOP :面向切面编程在程序设计中,我们需要满足高耦合低内聚,所以编程需满足六大原则,一个法则.AOP面向切面编程正是为了满足这些原则的一种
- 问题:最近在项目中遇到,不同客户机安装不同Office版本,在导出Excel时,发生错误。找不到Excel Com组件,错误信息如下。&nb
- 一.概述在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条
- 本文实例讲述了C#数字图像处理之图像二值化(彩色变黑白)的方法。分享给大家供大家参考。具体如下://定义图像二值化函数private sta
- 23种设计模式第十二篇:java模版方法模式定义:定义一个操作中算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变算法的结构即可重定义
- 本文实例为大家分享了java实现简单单链表的具体代码,供大家参考,具体内容如下一、定义:单链表是一种链式存取的数据结构,用一组地址任意的存储
- 一、MyBatis的逆向⼯程(1)所谓的逆向⼯程是:根据数据库表逆向⽣成Java的pojo类,SqlMapper.xml⽂件,以及Mappe
- 最近学习Spring,一直不太明白Srping的切面编程中的的argNames的含义,经过学习研究后,终于明白,分享一下需要监控的类:pac
- Spring MVC高级技术包括但不限于web.xml配置、异常处理、跨重定向请求传递数据1、web.xml文件的配置<!DOCTYP
- flutter中的布局flutter布局机制的核心是组件。在flutter中,几乎所有的东西都是组件,布局模型也不例外。图片,Icon, 文
- @RequestParam@RequestParam:接收来自RequestHeader中,即请求头。通常用于GET请求,例如:http:/
- 摘要本文主要介绍基于SpringBoot定时任务ScheduledTaskRegistrar的动态扩展,实现定时任务的动态新增和删除。Sch
- 一、只读自动属性(Read-only auto-properties) C# 6之前我们构建只读自动属性: public stri
- 有时候可能需要将手机上的一些操作投影出来,比如一些App Demo的展示等。其实,有专门的硬件设备能干这件事儿,但没必要专门为展示个Demo
- 概念里氏替换原则是任何基类出现的地方,子类一定可以替换它;是建立在基于抽象、多态、继承的基础复用的基石,该原则能够保证系统具有良好的拓展性,