1.什么是多租户? 项目中是怎么实现多租户的? 用到了哪些技术实现?
多租户是一种软件架构技术,多出现在saas平台的系统中,主要的场景是在多用户的环境下,共用一套系统并且要注意数据之间的隔离性。
实现方案:项目中实现多租户采用的是应用层数据库层共享数据隔离:
解释:应用程序和数据库只部署一套,所有租户共享即共同使用一个数据库和应用程序,而租户数据则使用表字段进行数据隔离。如,在表中增加TenantID多租户的数据字段,这是共享程度最高且隔离级别最低的模式。简单讲,每插入一条数据时都需要有一个租户的标识。这样才能在同一张表中区分出不同客户的数据,这也是项目中设计的【enterprise_id和store_id】。
采用的技术:mybatis-plus的TenantLineInnerceptor【拦截器】来实现的,dubbo的RPC上下文对象。当目标SQL在执行时,MybatisPlusInterceptor(自定义的拦截器实现了**TenantLineInnerceptor接口**
)会对商户ID和门店ID的数据进行拦截,然后再SQL语句后自动拼接条件,在CRUD时都会这样做。
具体实现1拦截器的配置:
1.创建**MybatisPlusConfig**
配置类;
2.把自定义拦截器插件**MybatisPlusInterceptor**
的对象交由Spring管理,对象添加企业ID字段拦截器插件、基于门店ID字段拦截器插件和分页插件后返回给spring容器。
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 如果用了分页插件注意
// 先 add TenantLineInnerInterceptor
// 再 add PaginationInnerInterceptor
// 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
//企业号租户字段插件
interceptor.addInnerInterceptor(tenantEnterpriseInterceptor());
//门店租户字段插件
interceptor.addInnerInterceptor(tenantStoreInterceptor());
//分页的插件
interceptor.addInnerInterceptor(paginationInnerInterceptor());
return interceptor;
}
3.基于企业号ID字段拦截器的实现:
1)在重写的**getTenantId()**
的方法中,使用dubbo的RPC上下文对象,获取当前用户信息,从用户信息获取当前用户的企业号信息,把企业号信息返回给企业号ID字段拦截器的对象。
2)在重写的**getTenantIdColumn()**
方法中,返回企业ID;
3)在重写的**ignoreTable()**
的方法中,获取需要忽略表名的集合,判断客户端的传递的表名是否在该集合中,在返回ture(表示忽略),不在(表示不忽略)且要企业ID字段不能为空后返回false;
@Bean
public TenantLineInnerInterceptor tenantEnterpriseInterceptor(){
return new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
//从当前的RPC上下文中获取用户信息
String currentUser = RpcContext.getContext().getAttachment(SuperConstant.CURRENT_USER);
if (EmptyUtil.isNullOrEmpty(currentUser)){
return null;
}
UserVo userVo = JSON.parseObject(currentUser, UserVo.class);
return new StringValue(String.valueOf(userVo.getEnterpriseId()));
}
@Override
public String getTenantIdColumn() {
return SuperConstant.ENTERPRISE_ID;
}
@Override
public boolean ignoreTable(String tableName) {
// 是否需要需要过滤某一张表
List<String> tableNameList = tenantProperties.getIgnoreEnterpriseTables();
if (!EmptyUtil.isNullOrEmpty(tableNameList)){
if (tableNameList.contains(tableName)){
return true;
}
}
//拼接的sql多租户字段标示对应的值不能为空
Expression tenantId = this.getTenantId();
if (EmptyUtil.isNullOrEmpty(tenantId)){
log.info("企业隐式传参为空,忽略表:{}",tableName);
return true;
}
return false;
}
});
}
4.基于门店D字段拦截器的实现:
1)在重写的**getTenantId()**
的方法中,使用dubbo的RPC上下文对象,获取当前用户信息,从用户信息获取当前用户的企业号信息,把企业号信息返回给企业号ID字段拦截器的对象。
2)在重写的**getTenantIdColumn()**
方法中,返回门店ID;
3)在重写的**ignoreTable()**
的方法中,获取需要忽略表名的集合,判断客户端的传递的表名是否在该集合中,在返回ture(表示忽略getTenantIdColumn),不在(表示不忽略getTenantIdColumn)且要企业ID字段不能为空后返回false;
@Bean
public TenantLineInnerInterceptor tenantStoreInterceptor(){
return new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
String currentStore = RpcContext.getContext().getAttachment(SuperConstant.CURRENT_STORE);
if (EmptyUtil.isNullOrEmpty(currentStore)){
return null;
}
return new StringValue(String.valueOf(currentStore));
}
@Override
public String getTenantIdColumn() {
return SuperConstant.STORE_ID;
}
@Override
public boolean ignoreTable(String tableName) {
// 是否需要需要过滤某一张表
List<String> tableNameList = tenantProperties.getIgnoreStoreTables();
if (!EmptyUtil.isNullOrEmpty(tableNameList)
&&tableNameList.contains(tableName)){
return true;
}
//拼接的sql多租户字段标示对应的值不能为空
Expression tenantId = this.getTenantId();
if (EmptyUtil.isNullOrEmpty(tenantId)){
log.info("门店隐式传参为空,忽略表:{}",tableName);
return true;
}
return false;
}
});
}
5.创建对应的忽略表属性配置类**TenantProperties**
;
具体实现2集成到项目服务中,以model-shop-producer为例:
1.在model-shop-producer模块中引入具体实现1拦截器配置的依赖;
<!--mybatis-plus支持-->
<dependency>
<groupId>com.itheima.restkeeper</groupId>
<artifactId>framework-mybatis-plus</artifactId>
</dependency>
2.在该模块下的配文件中添加忽略enterprise_id和store_id字段对应的表
2.各个系统平台如何分析数据库表是否需要忽略?
enterprisetorid和storeid两个字段拼装的有几种情况?忽略表选择的依据什么?
两个字段都需要拼装;只要enterpriseid拼装;只要storeid拼装;两个字段都不拼装。
选择的依据:1.先看数据库(一个一个表的看);2.找到哪个服务使用这个库;3.当前执行的业务功能是什么;4.当前的表需不需要做数据隔离。需要站在业务的角度,如在运营平台就需要站在运营商的角度,在商家平台就需要站在商家的角度
运营平台:
在model-basic-producer模块忽略表配置:
#忽略商户号表
ignore-enterprise-tables:
- tab_places
- tab_affix
- tab_log_business
- tab_data_dict
- tab_sms_blacklist
- tab_sms_channel
- tab_sms_send_record
- tab_sms_sign
- tab_sms_template
- undo_log
#忽略门店号表
ignore-store-tables:
- tab_places
- tab_affix
- tab_log_business
- tab_data_dict
- tab_sms_blacklist
- tab_sms_channel
- tab_sms_send_record
- tab_sms_sign
- tab_sms_template
- undo_log
model-security-producer模块中忽略表的配置
#忽略商户号表
ignore-enterprise-tables:
- tab_enterprise
- tab_resource
- tab_role
- tab_user
- tab_customer
- tab_role_resource
- tab_user_role
- undo_log
#忽略门店号表
ignore-store-tables:
- tab_enterprise
- tab_resource
- tab_role
- tab_user
- tab_customer
- tab_role_resource
- tab_user_role
- undo_log
商家平台:
model-shop-producer模块中忽略表的配置
#忽略商户号表
ignore-enterprise-tables:
- tab_dish_flavor
- tab_order_item
- undo_log
#忽略门店号表
ignore-store-tables:
- tab_dish_flavor
- tab_brand
- tab_order_item
- tab_store
- undo_log
model-shop-user模块中忽略表的配置
#忽略商户号表
ignore-enterprise-tables:
- tab_role
- tab_user_role
- undo_log
#忽略门店号表
ignore-store-tables:
- tab_user
- tab_customer
- tab_role
- tab_user_role
- undo_log
点餐平台:(因为进店点餐直接获取到了table_id)
model-shop-applet模块中忽略表的配置
#忽略商户号表
ignore-enterprise-tables:
- tab_brand
- tab_category
- tab_dish
- tab_dish_flavor
- tab_order
- tab_order_item
- tab_printer
- tab_printer_dish
- tab_store
- tab_table
- tab_table_area
- tab_role
- tab_user_role
- undo_log
#忽略门店号表
ignore-store-tables:
- tab_brand
- tab_category
- tab_dish
- tab_dish_flavor
- tab_order
- tab_order_item
- tab_printer
- tab_printer_dish
- tab_store
- tab_table
- tab_table_area
- tab_role
- tab_user_role
- undo_log
3.我们平台租户字段有几个,分别是如何传递到各个服务工程的?
有两个字段enterprise_id(企业号)和store_id(门店id).
企业标识传递:
1.在客户端用户启用了认证服务后,在**JsonServerAuthenticationSuccessHandler**
认证服务转换器中,在构建用户信息时写入用户的企业ID;
2.在用户认证成功后,把企业ID初始化到JwtToken(包含了用户信息,用户的企业ID和门店ID);
3.JwtToken会传递给客户端(这一步代码由前端书写),以map的格式存储在客户端的localStorage中;
4.在用户发起request请求时,通过拦截每次请求携带的JwtToken,认证该用户是否合法;
5.用户认证通过进入spring的gateway网关,在网关中使用【全局过滤器filter】拦截请求,解析请求头中的JwtToken中的user信息,解析完毕后使用http协议转发到web层,此时用户信息时存储在请求头中的;
6.在web层请求被springMVC拦截器拦截,获取到请求头中的【用户信息】;
7.在**TenantIntercept**
类中从请求拿到用户信息,并放入dubbo的上下文对象即**RpcContext**
中;
8.使用mybatis-plus多租户插件拦截,获取到用户的企业id后拼装到对应执行的SQL条件。
门店标识传递:
1.在客户端用户启用了认证服务后,在**JsonServerAuthenticationSuccessHandler**
认证服务转换器中,在构建用户信息时写入用户的门店D;
2.在用户认证成功后,把企业ID初始化到JwtToken(包含了用户信息,用户的门店ID);
3.JwtToken会传递给客户端(这一步代码由前端书写),以map的格式存储在客户端的localStorage中,并且会单独创建一个key:store_id来存储门店id(值是前端从JwtToken中提取出来的);注意点:当用户点击切换门店后,在JwtToken中门店ID就会发生改变。
4.在用户发起request请求时,通过拦截每次请求携带的JwtToken,认证该用户是否合法;
5.用户认证通过进入spring的gateway网关,在网关中使用【全局过滤器filter】拦截请求,解析请求头中的JwtToken中的user信息,解析完毕后使用http协议转发到web层,此时用户信息时存储在请求头中的;
6.在web层请求被springMVC拦截器拦截,获取到请求头中的【用户信息】;
7.在**TenantIntercept**
类中从请求拿到用户信息,并放入dubbo的上下文对象即**RpcContext**
中;
8.使用mybatis-plus多租户插件拦截,获取到用户的门店id后拼装到对应执行的SQL条件。