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容器。
@Beanpublic 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;
@Beanpublic TenantLineInnerInterceptor tenantEnterpriseInterceptor(){return new TenantLineInnerInterceptor(new TenantLineHandler() {@Overridepublic 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()));}@Overridepublic String getTenantIdColumn() {return SuperConstant.ENTERPRISE_ID;}@Overridepublic 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;
@Beanpublic TenantLineInnerInterceptor tenantStoreInterceptor(){return new TenantLineInnerInterceptor(new TenantLineHandler() {@Overridepublic Expression getTenantId() {String currentStore = RpcContext.getContext().getAttachment(SuperConstant.CURRENT_STORE);if (EmptyUtil.isNullOrEmpty(currentStore)){return null;}return new StringValue(String.valueOf(currentStore));}@Overridepublic String getTenantIdColumn() {return SuperConstant.STORE_ID;}@Overridepublic 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_logmodel-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_logmodel-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条件。
