大型SaaS系统的数据库许可设计与实施!
多租户是SaaS中的一个重要概念。它是一种软件架构技术,将同一系统分布在各个租户之间,并隔离租户之间的数据。换句话说,一个租户无法访问另一个租户的数据。根据隔离级别的不同,通常有三种实现方案:
- 每个租户使用独立的DataBase,隔离级别高且好,但成本较高
- DataBase分布在租户s之间,使用独立的Schema
- 架构在 租户s 之间分配,并且 租户 字段添加到表中。数据共享级别最高,隔离级别最低。
数据库设计
Mybatis-plus提供了基于三级插件分页的多租户方案,会通知。正式开始之前,先准备创建两张表,并在主字段后添加字段 租户 Kongfun_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>
Application‶❀ class:@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;
}
}
@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解析器
- 安装租户处理器,单独管理租户逻辑
租户暂时驻扎在这里。 3 进行测试。尝试执行全表语句:
public List<User> getUserList() {
return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
}
使用插件读取完整的SQL语句。可以看到租户过滤器的条件是自动添加在问题条件后面的: 至于服务器,我们可以从缓存或者请求头中获取。视情况而定。以请求头为例:
@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请求时,会将tenant Id字段插入到Header中,后端从处理器接收。。 ,设置为租户针对今天的要求的审查标准。
如果请求头中带有租户的个人资料,你可能会发现使用它时会遇到一些陷阱。如果使用多个线程,新开启的异步线程将不会承载当前线程的请求。
@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;
}
通过执行上面的方法,发现没有收到当前请求,所以没有收到租户 ID,从而导致出现如下空指针异常:
编辑起来也很简单,启用子RequestAttributes 的 -thread 部分,修改上面的代码:
@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表中Filter:
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();
同样,租户 的过滤条件添加到左右表中:
查看公共请求table without Enter:
@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语句中手动添加租户过滤器。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。