一、多租户基本简介
多租户是一种有选择性的数据隔离技术,可以保证系统共性的部分被共享,个性的部分被单独隔离。
多租户在数据存储上存在三种主要的方案,分别是:
- 独立数据库 一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本也高。
- 共享数据库,独立 Schema 即所有租户共享数据库,但一个租户一个 Schema。
- 共享数据库,共享 Schema 即租户共享同一个数据库、同一个 Schema,但在表中通过 TenantID 区分租户的数据。
二、pigx 多租户实现原理
pigx 采用的是第 3 种方案,即共享数据库、共享 schema,在表中通过 tenant_id 字段来实现多租户数据隔离
图片来源
1、如何确定租户的请求
如果把后端抽象的看成服务的集合,那么这些服务可以分为多租户相关服务与共享租户相关服务
PIGX 通过在前端请求时头部带上 TANENT-ID 报文,来确定租户的请求:
后端服务通过 TenantContextHolderFilter 过滤器将请求中的 tenant_id 值拿到(如果为空则采用默认值为 1 的租户),并通过 TenantContextHolder 放入上下文中,请求下游的业务便可通过 TenantContextHolder.getTenantId()获取当前租户
TenantContextHolderFilter 部分源码:
@Override
@SneakyThrows
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader(CommonConstants.TENANT_ID);
log.debug("获取 header 中的租户 ID 为:{}", tenantId);
if (StrUtil.isNotBlank(tenantId)) {TenantContextHolder.setTenantId(Integer.parseInt(tenantId));
} else {
// 当请求头部未包含 tenant_id 值时,使用默认租户值 1:CommonConstants.TENANT_ID_1
TenantContextHolder.setTenantId(CommonConstants.TENANT_ID_1);
}
filterChain.doFilter(request, response);
TenantContextHolder.clear();}
2、如何在微服务中传递租户的请求
上面所讲的只是请求从前端到后端时,租户的信息是如何确定的过程。但是还没有解决后端服务间调用时,租户信息的传递问题
pigx 是通过请求链路信息来维护租户信息传递的,pigx 中使用 feign 实现服务间内部调用,通过对 RequestInterceptor 实现JiyupFeignTenantInterceptor,使 feign 中调用服务时像上面的前端请求一样,在头部也带上了上游的TANENT-ID 报文:
JiyupFeignTenantInterceptor:
@Override
public void apply(RequestTemplate requestTemplate) {if (TenantContextHolder.getTenantId() == null) {log.error("TTL 中的 租户 ID 为空,feign 拦截器 >> 增强失败");
return;
}
requestTemplate.header(CommonConstants.TENANT_ID, TenantContextHolder.getTenantId().toString());
}
由此可见:
- 租户的最初来源是前端发送的报文头
- TenantContextHolderFilter 负责报文头的承上、JiyupFeignTenantInterceptor 报文头的启下、TenantContextHolder 是承上启下的容器
- 服务链路中的所有业务代码,得益于以上机制的帮助,直接通过 TenantContextHolder.getTenantId()便可获得当前租户
3、如何实现租户数据逻辑
多租户的底层实现逻辑是通过使用 MyBatis-Plus 的分页插件(PaginationInterceptor)实现的
在 MybatisPlusConfiguration 中配置PaginationInterceptor
@Configuration
@ConditionalOnBean(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisPlusConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "mybatisPlus.tenantEnable", havingValue = "true", matchIfMissing = true)
public PaginationInterceptor paginationInterceptor(JiyupTenantHandler tenantHandler) {PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
List<ISqlParser> sqlParserList = new ArrayList<>();
TenantSqlParser tenantSqlParser = new TenantSqlParser();
tenantSqlParser.setTenantHandler(tenantHandler);
sqlParserList.add(tenantSqlParser);
paginationInterceptor.setSqlParserList(sqlParserList);
return paginationInterceptor;
}
}
插件接收一个 SQL 解析器(ISqlParse)集合,每增加一种解析业务就应该向该集合中增加一个解析器的实现
这里只加入了租户 SQL 解析器(TenantSqlParser),该解析器负责实现租户的数据处理逻辑
同时 TenantSqlParser 将需要由外部业务所决定的部分通过 TenantHandler 接口提取出来:
/**
* 租户处理器(TenantId 行级)*
* @author hubin
* @since 2017-08-31
*/
public interface TenantHandler {
/**
* 获取租户 ID 值表达式,支持多个 ID 条件查询
* <p>
* 支持自定义表达式,比如: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);
}
JiyupTenantHandler实现了TenantHandler,通过了解该实现逻辑可以得出以下结论:
- 实现租户数据处理逻辑时,租户 ID 从 TenantContextHolder.getTenantId()方法中获取
这就解释了为何 TenantContextHolderFilter 与 JiyupFeignTenantInterceptor 要承上启下的保证整个服务链路中的租户信息不丢失,因为任何一个服务的底层租户数据处理逻辑都需要用到 TenantContextHolder 中的租户信息
- pigx 平台中的租户列名统一使用“tenant_id”
- pigx 平台中的多租户表(带 tenant_id 的表)需在配置中进行配置才能生效
比如 pigx-upms-biz-dev.yml 中的以下配置,就是 JiyupTenantHandler 实现类中所需要的:
# 租户表维护
jiyup:
tenant:
column: tenant_id
tables:
- sys_user
- sys_role
- sys_dept
- sys_log
- sys_social_details
- sys_dict
- sys_dict_item
- sys_public_param
- sys_log
- sys_file
4、总结
以上就是 pigx 实现多租户的基本原理,以下图示简单描述了实现过程:
参考
多租户设计
mybatis-plus 多租户解析器~~~~