【解决方案】多租户技能架构设计入门(二)

打印 上一主题 下一主题

主题 2017|帖子 2017|积分 6051

目次

前言

对于整个多租户技能架构的设计而言,笔者认为最关键的就是 3 点:底层数据隔离模式(策略) + 同一的用户&权限体系(认证鉴权) + 业务层调用时的举动隔离(请求拦截)。
其次可以拓展的有:租户管理体系 + 流派体系 + 角色配置中心等。基本的一些概念我在上篇文章中已经有过较为详细的先容,此处便不再赘述了。
作为入门系列的第二篇,本文主要分享的是在业务体系的应用内部如何对多数据源进行切换,而底层的数据库硬件资源管理这部分会简朴带过(一样寻常由运维团队来负责搭建)。
下面我就从多数据源设计、技能选型、应用配置、具体实现这几个方面来做一个详细的分享。
一、多数据源设计

1.1概念模型

首先我们要先明白:全部接进来的租户,都是利用同一套代码,即同一套服务,但每个租户会拥有属于自己的数据库。
本小节先先容概念模型,数据隔离模式的分析会在1.2小节展开。
在单租户的时候,每个体系只为一个客户服务,我们只需要在每个业务体系的配置文件上写一个数据库毗连,就可以确保该体系的数据会进到这个对应的库表里。
在多租户的背景下,这里全部业务体系也都只有一个毗连,即一个多数据源库,根据体系所在的不同环境和租户毗连不同的库。
这个库内里只有一张表,每一行数据里放的是全部业务体系各自的数据库毗连,这个设计是不同的体系找到各自库Url毗连的第一步。
下面对几个关键的字段进行解读:

  • system_code:每个业务体系的标识,要求唯一
  • data_code:其实就是数据源的标识,一样寻常利用租户编码作为标识
  • data_name:体系的中文名称,更有助于区别是哪个体系
  • data_url:每个体系对应的数据库毗连 url 地址
  • data_env:所属的运行环境,可以分为 dev、test 和 prod 这3种
怎么样才能让每个体系找到属于自己的库呢?请看本文的第二、三、四这3个小节。
1.2隔离模式分析

结论先行:本文采用的是共享数据库实例独立数据架构的隔离模式。即:全部业务体系的数据都在一个数据库实例集群中,但是一个数据库实例内里可以有很多个数据库,且可以根据租户对每个数据库做权限组控制。原因主要有以下几点:

  • 数据量的要求:租户多、体系多、用户量大
  • 隔离度的要求:要求较高,行业的特殊性会对数据安全比较敏感
  • 业务的复杂度:关联的体系多达上百个,上下游的数据交互十分频仍
  • 成本的思量:成本虽可以负担,但既要满足上面几点要求,又不能太贵
  • 便于计量计费:有了各自的数据库,方便对客户做计量计费的统计
数据库实例集群的规格要高、性能要强,目前主流云厂商如阿里云和华为云等,都有自己 MySQL for RDS 产品,基本可以完美解决数据隔离和数据库角色权限的需求。
二、技能选型

结论先行:选择 baomidou(对,Mybatis Plus 就是他们的杰作) 下的 Dynamic Datasource 动态数据库方案,引入 3 个依靠:
  1.         <dependency>
  2.             <groupId>com.baomidou</groupId>
  3.             <artifactId>dynamic-datasource-spring</artifactId>
  4.             <version>4.2.0</version>
  5.         </dependency>
  6.         <dependency>
  7.             <groupId>com.baomidou</groupId>
  8.             <artifactId>dynamic-datasource-creator</artifactId>
  9.             <version>4.2.0</version>
  10.         </dependency>
  11.         <dependency>
  12.             <groupId>com.baomidou</groupId>
  13.             <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  14.             <version>4.2.0</version>
  15.         </dependency>
复制代码
Maven 的中央堆栈:https://mvnrepository.com/ 搜索关键词,如下图所示:
多数据源切换技能选型下面先容几个焦点的类以及 api:
  1.     //从请求头中获取当前租户编码
  2.     String tenantCode = request.getHeader("Tenantcode");
  3.     //切换多数据源的核心工具类,此处将租户编码作为数据源key
  4.     DynamicDataSourceContextHolder.push(tenantCode);
复制代码
  1.     public DynamicRoutingDataSource dataSource() {
  2.         //根据特定的规则选择要使用的数据源标识(如数据库名称、租户编码等),根据路由规则,每个数据访问操作将使用相应的数据源
  3.         DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(Collections.emptyList());
  4.         DataSourceProperty dataSourceProperty = new DataSourceProperty();
  5.         //配置文件的 driver-class-name 驱动名
  6.         dataSourceProperty.setDriverClassName(this.dataSourceProperties.getDriverClassName());
  7.         //数据库连接 url
  8.         dataSourceProperty.setUrl(this.dataSourceProperties.getUrl());
  9.         //连接数据库的用户名/密码
  10.         dataSourceProperty.setUsername(this.dataSourceProperties.getUsername());
  11.         dataSourceProperty.setPassword(this.dataSourceProperties.getPassword());
  12.         //创建多数据源连接,即所有的数据源都可以获取到
  13.         DataSource ds = dataSourceCreator.createDataSource(dataSourceProperty);
  14.         dynamicRoutingDataSource.addDataSource(this.dynamicDataSourceProperties.getPrimary(), ds);
  15.         return dynamicRoutingDataSource;
  16.     }
复制代码
三、应用配置

相较于 Spring 的各种 xml 配置,Spring boot 引入的约定大于配置的这一重大升级,是多数据源切换的紧张基础。
之前单租户的时候,无论是分布式的单体还是微服务,应用的 application.yml 大概 application.properties 都有一个本体系的数据库毗连。外部发起请求大概被别的体系调用时,本体系产生的数据都会根据这个配置文件里的数据库毗连去进行增删改查。具体如下:
  1. spring:
  2.   datasource:
  3.     driver-class-name: com.mysql.cj.jdbc.Driver
  4.     url: jdbc:mysql://host:port/本系统的数据库名称
  5.     username: 本系统数据库账号
  6.     password: 本系统数据库密码
复制代码
那么,在多租户下,是不是有多少个租户就要在 applicationyml 里写多少个数据库毗连呢?
答案固然是否定的。
基于第一章选择的数据隔离模式,显然将每个租户的数据库毗连都维护在一个地方是最方便的,于是便有了第一章的多数据源库。
所以,基于多租户的业务体系的 applicationyml  里该怎么写数据库毗连呢?可以这样写:
  1. spring:
  2.   datasource:
  3.     driver-class-name: com.mysql.cj.jdbc.Driver
  4.     url: jdbc:mysql://host:port/多数据源库名称
  5.     username: 多数据源库账号
  6.     password: 多数据源库密码
  7. initial:
  8.   saas:
  9.     system-code: springboot-initial ##这是系统的唯一标识,很关键
复制代码
这样就可以根据租户标识(data_code)与体系标识(system_code)来唯一确定属于本体系的数据库了,具体怎么做,下一节会给出 demo。
四、具体实现

牢牢把握这 4 点:请求拦截 + 租户编码 + 本地线程 + 切换数据源。这4点贯穿了整个多租户数据源切换的全过程,是数据源切换策略的焦点。
由于篇幅,以下只演示焦点的步骤:

  • 拦截器+租户编码
    1. public class TenantInterceptor implements HandlerInterceptor {
    2.     @Override
    3.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    4.         boolean predHandle = super.preHandle(request, response, handler);
    5.         if (!CorsUtils.isPreFlightRequest(request)) {
    6.             String tenantHeader = request.getHeader("Tenantcode");
    7.             if (StringUtils.isBlank(tenantHeader)) {
    8.                 throw new RuntimeException("请求错误");
    9.             }
    10.             //本地线程设置值
    11.             ThreadLocalUtils.setValue(tenantCode);
    12.             //切换多数据源的核心类,此处将租户编码作为数据源 key
    13.             DynamicDataSourceContextHolder.push(tenantCode);
    14.         }
    15.         return predHandle;
    16.     }
    17.     @Override
    18.     public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
    19.                                 Object handler, Exception ex) throws Exception {
    20.         super.afterCompletion(request, response, handler, ex);
    21.         //请求完成后清除
    22.         ThreadLocalUtils.removeValue();
    23.         //同样是清除本次调用线程中的数据源 key
    24.         DynamicDataSourceContextHolder.clear();
    25.     }
    26. }
    复制代码
  • 本地线程
    1. public class ThreadLocalUtils {
    2.     /**
    3.      * 不熟悉的同学可以再去复习一下 ThreadLocal 的相关知识
    4.      */
    5.     private static final ThreadLocal<String> THREADLOCAL = new ThreadLocal<>();
    6.     public static void setValue(String value) {
    7.         THREADLOCAL.set(value);
    8.     }
    9.     public static String getValue() {
    10.         return THREADLOCAL.get();
    11.     }
    12.     public static void removeValue() {
    13.         THREADLOCAL.remove();
    14.     }
    15. }
    复制代码
  • 切换数据源
    这里其实就是第一节中那张多数据源库表的具体实现,实现类还 implements 了 InitializingBean 所以会有 afterPropertiesSet() 方法。
    1.     @Override
    2.     public void afterPropertiesSet() {
    3.         LambdaQueryWrapper<DynamicTenantDatasource> wrapper = new LambdaQueryWrapper<>();
    4.         RunTimeEnv env = RunEnv.searchRunEnv(Collections.singletonList(this.environment.getActiveProfiles()));
    5.         log.info("当前数据源所处环境:{}", env);
    6.         assert env != null;
    7.         wrapper.eq(DynamicTenantDatasource::getRunTimeEnv, env.getValue())
    8.                 .eq(DynamicTenantDatasource::getSystemCode, this.dynamicProperties.getSystemCode());
    9.         this.list(wrapper).forEach(val -> {
    10.             DataSourceProperty dataSourceProperty = new DataSourceProperty();
    11.             //下面是数据源配置
    12.             dataSourceProperty.setDriverClassName(val.getDriverClassName());
    13.             dataSourceProperty.setUrl(val.getUrl());
    14.             dataSourceProperty.setUsername(val.getUserName());
    15.             dataSourceProperty.setPassword(val.getPassword());
    16.             DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
    17.             //这里就会拿到当前系统的所有租户编码了
    18.             this.dynamicRoutingDataSource.addDataSource(val.getDataCode(), dataSource);
    19.         });
    20.     }
    复制代码
由于在请求颠末拦截器的时候,当前线程已经获取了当前的租户编码,且已经将这个租户编码push到了多数据源工具类,那么只要本次请求涉及到数据库操作,就能唯一确定数据源了,即能唯一确定本次数据会毗连到具体哪个库。
五、文章小结

假如你也对基于多租户的动态数据源切换有过思索,那么盼望我们的思维能迸出一些火花。
作为整个多租户的数据隔离模式的紧张部分,本篇文章尽可能地将笔者的思索由浅到深与各人分享。为了实现整个数据隔离模式的落地,需要大量的实践来论证可行性,并且需要相当的资源投入才能真正作为成熟的框架部署到生产环境。
此中就少不了运维团队以及云原生团队的支持,基于K8s容器的服务治理、镜像打包、连续的 CI/CD(GitLab+Jenkins)以及整个 DevOps 平台的搭建,才能让这套方案和架构发挥最大的作用。
接下来请期待本系列文章的续作,文章如有不敷和错误,还请各人指正。大概你有其它想说的,也欢迎各人在评论区交流!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

鼠扑

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表