Code前端首页关于Code前端联系我们

大型SaaS系统的数据库许可设计与实施!

terry 2年前 (2023-09-24) 阅读数 59 #后端开发

多租户是SaaS中的一个重要概念。它是一种软件架构技术,将同一系统分布在各个租户之间,并隔离租户之间的数据。换句话说,一个租户无法访问另一个租户的数据。根据隔离级别的不同,通常有三种实现方案:

  1. 每个租户使用独立的DataBase,隔离级别高且好,但成本较高
  2. DataBase分布在租户s之间,使用独立的Schema
  3. 架构在 租户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;
    }
}

这里实现的主要功能:

  • 创建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,从而导致出现如下空指针异常: 大型SaaS系统的数据范围权限设计与实现!

编辑起来也很简单,启用子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:大型SaaS系统的数据范围权限设计与实现!

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立即添加另一个租户查询条件,这会导致错误:大型SaaS系统的数据范围权限设计与实现!

我们可以修改语句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();

看执行结果。可以看到 租户 的查询条件自动包含在子查询中: 大型SaaS系统的数据范围权限设计与实现!

使用 Join 查看表查询:

@Select("select u.* from user u left join user_snapshot us on u.id=us.id")
List<User> selectSnapshot();

同样,租户 的过滤条件添加到左右表中: 大型SaaS系统的数据范围权限设计与实现!

查看公共请求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();
大型SaaS系统的数据范围权限设计与实现!

查看执行结果,原来,在这种情况下,租户的过滤条件被添加到'FROM关键字后的第一个表中。因此,如果使用这种施用方法,则需要多加注意。用户需要在SQL语句中手动添加租户过滤器。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门