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容器。

  1. @Bean
  2. public MybatisPlusInterceptor mybatisPlusInterceptor() {
  3. MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  4. // 如果用了分页插件注意
  5. // 先 add TenantLineInnerInterceptor
  6. // 再 add PaginationInnerInterceptor
  7. // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
  8. //企业号租户字段插件
  9. interceptor.addInnerInterceptor(tenantEnterpriseInterceptor());
  10. //门店租户字段插件
  11. interceptor.addInnerInterceptor(tenantStoreInterceptor());
  12. //分页的插件
  13. interceptor.addInnerInterceptor(paginationInnerInterceptor());
  14. return interceptor;
  15. }

3.基于企业号ID字段拦截器的实现:
1)在重写的**getTenantId()**的方法中,使用dubbo的RPC上下文对象,获取当前用户信息,从用户信息获取当前用户的企业号信息,把企业号信息返回给企业号ID字段拦截器的对象。
2)在重写的**getTenantIdColumn()**方法中,返回企业ID;
3)在重写的**ignoreTable()**的方法中,获取需要忽略表名的集合,判断客户端的传递的表名是否在该集合中,在返回ture(表示忽略),不在(表示不忽略)且要企业ID字段不能为空后返回false;

  1. @Bean
  2. public TenantLineInnerInterceptor tenantEnterpriseInterceptor(){
  3. return new TenantLineInnerInterceptor(new TenantLineHandler() {
  4. @Override
  5. public Expression getTenantId() {
  6. //从当前的RPC上下文中获取用户信息
  7. String currentUser = RpcContext.getContext().getAttachment(SuperConstant.CURRENT_USER);
  8. if (EmptyUtil.isNullOrEmpty(currentUser)){
  9. return null;
  10. }
  11. UserVo userVo = JSON.parseObject(currentUser, UserVo.class);
  12. return new StringValue(String.valueOf(userVo.getEnterpriseId()));
  13. }
  14. @Override
  15. public String getTenantIdColumn() {
  16. return SuperConstant.ENTERPRISE_ID;
  17. }
  18. @Override
  19. public boolean ignoreTable(String tableName) {
  20. // 是否需要需要过滤某一张表
  21. List<String> tableNameList = tenantProperties.getIgnoreEnterpriseTables();
  22. if (!EmptyUtil.isNullOrEmpty(tableNameList)){
  23. if (tableNameList.contains(tableName)){
  24. return true;
  25. }
  26. }
  27. //拼接的sql多租户字段标示对应的值不能为空
  28. Expression tenantId = this.getTenantId();
  29. if (EmptyUtil.isNullOrEmpty(tenantId)){
  30. log.info("企业隐式传参为空,忽略表:{}",tableName);
  31. return true;
  32. }
  33. return false;
  34. }
  35. });
  36. }

4.基于门店D字段拦截器的实现:
1)在重写的**getTenantId()**的方法中,使用dubbo的RPC上下文对象,获取当前用户信息,从用户信息获取当前用户的企业号信息,把企业号信息返回给企业号ID字段拦截器的对象。
2)在重写的**getTenantIdColumn()**方法中,返回门店ID;
3)在重写的**ignoreTable()**的方法中,获取需要忽略表名的集合,判断客户端的传递的表名是否在该集合中,在返回ture(表示忽略getTenantIdColumn),不在(表示不忽略getTenantIdColumn)且要企业ID字段不能为空后返回false;

  1. @Bean
  2. public TenantLineInnerInterceptor tenantStoreInterceptor(){
  3. return new TenantLineInnerInterceptor(new TenantLineHandler() {
  4. @Override
  5. public Expression getTenantId() {
  6. String currentStore = RpcContext.getContext().getAttachment(SuperConstant.CURRENT_STORE);
  7. if (EmptyUtil.isNullOrEmpty(currentStore)){
  8. return null;
  9. }
  10. return new StringValue(String.valueOf(currentStore));
  11. }
  12. @Override
  13. public String getTenantIdColumn() {
  14. return SuperConstant.STORE_ID;
  15. }
  16. @Override
  17. public boolean ignoreTable(String tableName) {
  18. // 是否需要需要过滤某一张表
  19. List<String> tableNameList = tenantProperties.getIgnoreStoreTables();
  20. if (!EmptyUtil.isNullOrEmpty(tableNameList)
  21. &&tableNameList.contains(tableName)){
  22. return true;
  23. }
  24. //拼接的sql多租户字段标示对应的值不能为空
  25. Expression tenantId = this.getTenantId();
  26. if (EmptyUtil.isNullOrEmpty(tenantId)){
  27. log.info("门店隐式传参为空,忽略表:{}",tableName);
  28. return true;
  29. }
  30. return false;
  31. }
  32. });
  33. }

5.创建对应的忽略表属性配置类**TenantProperties**

具体实现2集成到项目服务中,以model-shop-producer为例:
1.在model-shop-producer模块中引入具体实现1拦截器配置的依赖;

  1. <!--mybatis-plus支持-->
  2. <dependency>
  3. <groupId>com.itheima.restkeeper</groupId>
  4. <artifactId>framework-mybatis-plus</artifactId>
  5. </dependency>

2.在该模块下的配文件中添加忽略enterprise_id和store_id字段对应的表

2.各个系统平台如何分析数据库表是否需要忽略?

enterprisetorid和storeid两个字段拼装的有几种情况?忽略表选择的依据什么?
两个字段都需要拼装;只要enterpriseid拼装;只要storeid拼装;两个字段都不拼装。
选择的依据:1.先看数据库(一个一个表的看);2.找到哪个服务使用这个库;3.当前执行的业务功能是什么;4.当前的表需不需要做数据隔离。需要站在业务的角度,如在运营平台就需要站在运营商的角度,在商家平台就需要站在商家的角度
运营平台:

  1. 在model-basic-producer模块忽略表配置:
  2. #忽略商户号表
  3. ignore-enterprise-tables:
  4. - tab_places
  5. - tab_affix
  6. - tab_log_business
  7. - tab_data_dict
  8. - tab_sms_blacklist
  9. - tab_sms_channel
  10. - tab_sms_send_record
  11. - tab_sms_sign
  12. - tab_sms_template
  13. - undo_log
  14. #忽略门店号表
  15. ignore-store-tables:
  16. - tab_places
  17. - tab_affix
  18. - tab_log_business
  19. - tab_data_dict
  20. - tab_sms_blacklist
  21. - tab_sms_channel
  22. - tab_sms_send_record
  23. - tab_sms_sign
  24. - tab_sms_template
  25. - undo_log
  26. model-security-producer模块中忽略表的配置
  27. #忽略商户号表
  28. ignore-enterprise-tables:
  29. - tab_enterprise
  30. - tab_resource
  31. - tab_role
  32. - tab_user
  33. - tab_customer
  34. - tab_role_resource
  35. - tab_user_role
  36. - undo_log
  37. #忽略门店号表
  38. ignore-store-tables:
  39. - tab_enterprise
  40. - tab_resource
  41. - tab_role
  42. - tab_user
  43. - tab_customer
  44. - tab_role_resource
  45. - tab_user_role
  46. - undo_log

商家平台:

  1. model-shop-producer模块中忽略表的配置
  2. #忽略商户号表
  3. ignore-enterprise-tables:
  4. - tab_dish_flavor
  5. - tab_order_item
  6. - undo_log
  7. #忽略门店号表
  8. ignore-store-tables:
  9. - tab_dish_flavor
  10. - tab_brand
  11. - tab_order_item
  12. - tab_store
  13. - undo_log
  14. model-shop-user模块中忽略表的配置
  15. #忽略商户号表
  16. ignore-enterprise-tables:
  17. - tab_role
  18. - tab_user_role
  19. - undo_log
  20. #忽略门店号表
  21. ignore-store-tables:
  22. - tab_user
  23. - tab_customer
  24. - tab_role
  25. - tab_user_role
  26. - undo_log

点餐平台:(因为进店点餐直接获取到了table_id)

  1. model-shop-applet模块中忽略表的配置
  2. #忽略商户号表
  3. ignore-enterprise-tables:
  4. - tab_brand
  5. - tab_category
  6. - tab_dish
  7. - tab_dish_flavor
  8. - tab_order
  9. - tab_order_item
  10. - tab_printer
  11. - tab_printer_dish
  12. - tab_store
  13. - tab_table
  14. - tab_table_area
  15. - tab_role
  16. - tab_user_role
  17. - undo_log
  18. #忽略门店号表
  19. ignore-store-tables:
  20. - tab_brand
  21. - tab_category
  22. - tab_dish
  23. - tab_dish_flavor
  24. - tab_order
  25. - tab_order_item
  26. - tab_printer
  27. - tab_printer_dish
  28. - tab_store
  29. - tab_table
  30. - tab_table_area
  31. - tab_role
  32. - tab_user_role
  33. - undo_log

3.我们平台租户字段有几个,分别是如何传递到各个服务工程的?

有两个字段enterprise_id(企业号)和store_id(门店id).
企业标识传递:
企业标识传递流程.png
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条件。

门店标识传递:
门店标识传递流程.png
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条件。