多租户的数据隔离方案,不外乎以下三种方案(来自网络转载)分别是:
也称per-database-per-tenant,即一个租户一个数据库实例。
这是第一种方案,即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。
优点: 为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
缺点: 增多了数据库的安装数量,随之带来维护成本和购置成本的增加。这种方案与传统的一个客户、一套数据、一套部署类似,差别只在于软件统一部署在运营商那里。如果面对的是银行、医院等需要非常高数据隔离级别的租户,可以选择这种模式,提高租用的定价。如果定价较低,产品走低价路线,这种方案一般对运营商来说是无法承受的。
也称per-schema-per-tenant,即一个租户一个schema,但都共享同一个数据库实例。
这是第二种方案,即多个或所有租户共享 DataBase,但是每个租户一个 Schema(也可叫做一个user)。
在MySQL中,schema和database是同义词.
CREATE SCHEMA和CREATE DATABASE是等效的.
但是其他的数据库产品(几乎所有数据库)有所不同.在oracle数据库产品中,schema是database的一部分.
表示the tables and other objects owned by a single user.
优点: 为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;
缺点: 如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据;如果需要跨租户统计数据,存在一定困难。
即所有租户都使用一个表,然后通过在所有表中增加一个字段(通常就是租户id)来区分不同租户。数据库实例和表都是共享的。
这是第三种方案,即租户共享同一个 DataBase、同一个 Schema,但在表中增加了租户ID
的多租户的数据字段。这是共享程度最高、隔离级别最低、维护成本和购置最低的模式。也是一种逻辑隔离方案。
优点: 三种方案比较,第三种方案的维护和购置成本最低,跨租户统计方便,允许每个数据库支持的租户数量多。
缺点: 隔离级别最低,需要在设计开发时加大对安全的开发量;对单个租户的数据备份和恢复困难。
如果希望以最少的服务器为最多的租户提供服务,并且租户接受牺牲隔离级别换取降低成本,这种方案最适合。
审视一下三种设计方案,从1到3隔离程度越来越低,共享程度越来越高。我们从以下一些维度对比一下它们各自的优劣(注意:对比主要是从数据库角度看的,而不是整个SaaS):
应用管理员, 也就是平台管理员
多租户SaaS系统怎么设计,下面是我总结的几点原则,供大家参考:
我们目前做到逻辑隔离,通过表里面增加租户ID的方式来实现多租户的支持。当然我们自然想做到物理隔离,相应的成本也会多很多。这块大家必须有租户间资源是隔离的概念。为了能更好的理解整个SaaS系统的设计初衷,我们可以认为租户间资源是物理隔离的。
每个租户是有各自的资源信息的,这些资源是租户私有的。比如:角色信息、用户信息、组织信息等。
租户和组织这块有很多相近的地方,这块需要深刻的理解下。
我个人是这么理解的:租户是对全部资源物理层面的隔离,而组织是对租户私有资源逻辑上的隔离。
为了降低系统的复杂性,我们建议租户不支持多层级,只能建一级,租户是有类型的,通过类型区分不同的业务场景,租户间是平等的。
比如:XXX运营方也是独立的租户,与其它用户无本质区别。
这块也是我们做的最大改动:轻租户,重组织。发挥组织的天然业务隔离的特性,通过组织树来实现资源数据权限。
组织是租户的私有资源,运营管理侧自然不应该去管理他,也不方便管理。
首先,我们为了租户能否方便的访问,以及平台能自动识别访问是哪个租户,我们在接入层采用通过url来识别租户。即系统在初始化租户信息时,会随机生成一个租户编码(租户编码允许修改一次),用于saas平台的三级域名监听,通过在业务系统的处理和绑定,当接收到请求时,拦截器会自动识别对应的租户编码,并加载对应的租户信息。
其次,在业务处理时,租户标识编号作为必须条件带入,进行数据操作。
后期,随着租户数量增多, 数据量必然指数级上涨,可以采用分库分表的处理策略。
首页:
- 区分平台端(
/platform/login
), 和租户端入口(/saas/login
)平台管理端:
- 新增租户管理, 包含租户基本信息, 租户角色, 租户权限管理,
- 1 – 租户基本信息, 要有租户编号,建议有个开放时间限制
- 新增运营管理:重视租户登录日志和操作日志的收集
- 业务修改:原来的业务逻辑中增加
租户ID
的字段平台租户端:
- 支持有各自的资源信息
Mybatis-plus
在第3层隔离级别上,提供了基于分页插件的多租户的解决方案,我们对此来进行介绍。参考: https://baomidou.com/guide/interceptor-tenant-line.html#tenantlineinnerinterceptor
在应用添加维护一张sys_tenant(租户管理表),在需要进行隔离的数据表上新增
租户id
;
public interface TenantHandler {
/**
* 获取租户 ID 值表达式,支持多个 ID 条件查询
*
* 支持自定义表达式,比如:tenant_id in (1,2) @since 2019-8-2
*
* @param where 参数 true 表示为 where 条件 false 表示为 insert 或者 select 条件
* @return 租户 ID 值表达式
*/
Expression getTenantId(boolean where);
/**
* 获取租户字段名
*
* @return 租户字段名
*/
String getTenantIdColumn();
/**
* 根据表名判断是否进行过滤
*
* @param tableName 表名
* @return 是否进行过滤, true:表示忽略,false:需要解析多租户字段
*/
boolean doTableFilter(String tableName);
}
PreTenantHandler 实现 TenantHandler
@Slf4j
@Component
public class PreTenantHandler implements TenantHandler {
@Autowired
private PreTenantConfigProperties configProperties;
/**
* 租户Id
*
* @return
*/
@Override
public Expression getTenantId(boolean where) {
//可以通过过滤器从请求中获取对应租户id
Long tenantId = PreTenantContextHolder.getCurrentTenantId();
log.debug("当前租户为{}", tenantId);
if (tenantId == null) {
return new NullValue();
}
return new LongValue(tenantId);
}
/**
* 租户字段名
*
* @return
*/
@Override
public String getTenantIdColumn() {
return configProperties.getTenantIdColumn();
}
/**
* 根据表名判断是否进行过滤
* 忽略掉一些表:如租户表(sys_tenant)本身不需要执行这样的处理
*
* @param tableName
* @return
*/
@Override
public boolean doTableFilter(String tableName) {
return configProperties.getIgnoreTenantTables().stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));
}
}
这里主要实现的功能:
- 创建SQL解析器集合
- 创建租户SQL解析器
- 设置租户处理器,具体处理租户逻辑
@EnableTransactionManagement
@Configuration
@MapperScan({
"com.xd.pre.**.mapper"})
public class MyBatisPlusConfig {
// 租户处理器
@Autowired
private PreTenantHandler preTenantHandler;
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
List<ISqlParser> sqlParserList = new ArrayList<>();
// 攻击 SQL 阻断解析器、加入解析链
sqlParserList.add(new BlockAttackSqlParser());
// 多租户拦截
TenantSqlParser tenantSqlParser = new TenantSqlParser();
tenantSqlParser.setTenantHandler(preTenantHandler);
sqlParserList.add(tenantSqlParser);
paginationInterceptor.setSqlParserList(sqlParserList);
return paginationInterceptor;
}
}
配置好之后,不管是查询、新增、修改删除方法,MP都会自动加上租户ID的标识,测试如下:
@Test
public void select(){
List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery().eq(User::getAge, 18));
users.forEach(System.out::println);
}
运行sql实例:
DEBUG==> Preparing: SELECT id, login_name, name, password,
email, salt, sex, age, phone, user_type, status,
organization_id, create_time, update_time, version,
tenant_id FROM sys_user
WHERE sys_user.tenant_id = '001' AND is_delete = '0' AND age = ?
如果在程序中,有部分SQL不需要加上租户ID的表示,需要过滤特定的sql,可以通过如下两种方式:
在配置分页插件中加上配置ISqlParserFilter解析器,如果配置SQL很多,比较麻烦,不建议。
//有部分SQL不需要加上租户ID的表示,需要过滤特定的sql。如果比较多不建议这里配置。
/*paginationInterceptor.setSqlParserFilter(new ISqlParserFilter() {
@Override
public boolean doFilter(MetaObject metaObject) {
MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
// 对应Mapper或者dao中的方法
if("com.erbadagang.mybatis.plus.tenant.mapper.UserMapper.selectList".equals(ms.getId())){
return true;
}
return false;
}
});*/
通过租户注解的形式,目前只能作用于Mapper的方法上。特定sql过滤 过滤特定的方法 也可以在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;
/**
*
* Mapper 接口
*
*
*/
public interface TenantMapper extends BaseMapper<Tenant> {
/**
* 自定Wrapper, @SqlParser(filter = true)注解代表不进行SQL解析也就没有租户的附加条件。
*
* @return
*/
@SqlParser(filter = true)
@Select("SELECT count(5) FROM t_tenant ")
public Integer myCount();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uns0Qsxd-1638525131388)(https://segmentfault.com/img/remote/1460000022320165)]
https://gitee.com/jinzheyi/yubb-saas
https://gitee.com/xiaoqiangBUG/hello-ruoyi-saas
https://gitee.com/Spring-Pig/RY-SAAS
https://baomidou.com/guide/interceptor-tenant-line.html#tenantlineinnerinterceptor
https://www.jianshu.com/p/1e2cef81bce8
https://docs.microsoft.com/zh-cn/azure/azure-sql/database/saas-tenancy-app-design-patterns
https://baomidou.com/guide/interceptor-tenant-line.html#tenantlineinnerinterceptor
https://gitee.com/baomidou/mybatis-plus-samples/tree/master/mybatis-plus-sample-tenant