文章目录
- 核心思路
- 方案选择
- 设计考量
- 安全性
- 扩展性
- 通用性
- 易用性
- 具体实现
- 租户信息透传
- 透传变量名命名规范
- 应用内透传
- 应用间透传
- 数据层租户隔离
- MySQL存储方案:多租户Mybatis插件
- Mybatis插件特点
- 使用多租户Mybatis插件的优势
- 参考文档
- 应用场景
经过工作中的一处场景启发,我进行了深入学习并总结出本篇文章。
核心思路
方案选择
多租户模型基本有以下三种(具体可以见这里),第一种形式隔离做的最彻底(isolated),多租户之间只在硬件层面进行共享;第三种形式共享做的最彻底(shared),在硬件、应用和DB层面都做到了共享。
一个应用要多租户化,选择何种形式,需要考虑具体的业务场景,同时也要考虑投入产出比,不同的形式投入成本不同,可以参考下面的图。上面的三种形式最左边可以认为是isolated approach,最右边是shared approach,第二种介于中间。越靠右的方式从初期来看投入成本更高,但从长久来看投入成本会更低。
设计考量
框架层面考量,如果从单纯项目实现层面也需要思考这些方面
安全性
对于一个多租户应用来说,任何业务处理、数据读写都需要有明确的租户上下文,这涉及到方方面面:
- 当前用户是否有特定租户的权限?
- 当前的web请求、RPC服务调用、任务启动、消息处理是否有特定的租户?
- 当前的数据读写是否有明确的租户参数?
一旦发现没有权限、没有租户的程序在执行,保护机制及时进行检验、发现并予以熔断报错。
扩展性
不同的租户,往往会有不同程度的差异,差异可能来自:
- 业务逻辑
- 数据模型
通用性
不同的租户有不同的需求和配置。如果多租户系统不能适应不同租户的需求,那么它就无法满足市场需求,也无法提供一致且可靠的服务。多租户的实现需要一次很好的抽象,系统化的思考和沉淀,把通用性做的好将极大发挥价值。
易用性
很多框架的建设因为太重最后被人放弃,多租户框架想走轻量级路线,以达到非常容易使用的目的。这要考虑很多的设计模式,比如让springboot红于一时的“约定胜于配置”(Convention Over Configuration),让使用者在80%的场景下使用起来非常简单,不用关心配置。多租户框架也可以做成一个jar包,要多租户化的应用只要引入即可。
具体实现
租户信息透传
专指各个代码执行链路上,租户信息的传递。为什么采用透传,而不是通过修改原有的函数签名,使用参数传递的方式完成租户信息传递?答案是显而易见的,租户隔离是面向整套系统的解决方案,全局的方法签名修改意味着巨大的改造成本。
租户信息透传就意味着需要封装一个全局的上下文对象,这个对象中需要包含应用需要用到的所有租户相关的必要信息,举例可以将这个对象命名为Context。同时国际化技术方案也有着同样的透传变量需求,再加上国际化相关的租户变量,Context中的变量及其命名规范如下。
透传变量名命名规范
变量 \ 作用域 | cookie | header(请求头) | 应用内 |
---|---|---|---|
租户标识 | ak_region | Region | region |
租户ID | \ | Region-Id | regionId |
租户名 | \ | Region-Name | regionName |
语言 | ak_user_locale | User-Locale | locale |
时区 | ak_user_timezone | User-Timezone | timezone |
用户工号 | ak_user_staff_id | User-Staff-Id | staffId |
要实现全链路的租户信息透传,就要解决各种各样的情形下,租户变量的传递问题。全链路租户信息透传,又分为应用内透传和应用间透传。
应用内透传
各自的应用有自己的上下文,也就是登陆态session,可以将租户的信息保存在应用上下文中。那么如果启用异步任务,其中需要上下文信息怎么办。
java应用中,参考trace类型应用的方案,采用ThreadLocal保存租户信息。为了防止异步线程丢失租户信息,我们封装了统一的线程池工具类,对jdk提供的原生线程池进行了封装,增加了租户信息透传特性。这里大致实现方式可以看我这一篇文章。
应用间透传
对于应用间的http接口调用,在client端,我们可以封装统一的http client,将Context中的内容放到请求的header中;在server端,我们提供了实现java-servlet-api标准的filter,实现从请求头中提取Context变量的功能。
对于各种消息中间件,如Kafka,RocketMQ等,也可以制定了租户变量透传的标准,以保证消息链路租户信息的完整。
数据层租户隔离
由于我们采用的是上文所说的“共享应用程序、共享数据库”的多租户模型,数据层的租户隔离是整个租户隔离技术体系中最重要的一环。对于每种数据源,我们需要向其schema中增加一个租户标识符,并在对该数据源的所有读写中,增加租户标识符的过滤。
对于MySQL这种存储介质,我们需要向每张表和视图中添加一个租户ID字段,并重写每一个SQL查询,向原有的过滤条件中增加租户ID的过滤,限制每一行数据只能被其所属的租户访问到,以保证数据安全性。
MySQL存储方案:多租户Mybatis插件
Mybatis插件特点
Mybatis插件支持拦截所有提交到dao层的SQL,并支持对提交的SQL进行改写,原理类似于Spring的Interceptor。
多租户Mybatis插件可以在运行时,动态获取到应用上下文中的租户变量,在执行查询时自动将region_id的筛选附加到SELCT语句的WHERE条件中;在执行写操作时,自动将region_id插入到数据表中。
使用多租户Mybatis插件的优势
- 使用Mybatis插件进行数据层租户隔离,可以大大降低业务代码在租户改造过程中的开发成本。
- 下面是使用Mybatis插件进行数据层隔离,与硬编码方式实现隔离的成本对比
使用Mybatis插件 | 硬编码 | |
---|---|---|
mapper文件 | 无需改造 | 所有SQL都需要改造,增加region_id字段 |
dao接口 | 无需改造 | 所有dao层接口都需要增加region_id参数 |
service层代码 | 大部分不需要改造 | 所有dao层接口调用,都需要显式传递region_id参数 |
参考文档
Mybatis插件文档
应用场景
-
云计算服务:云服务提供商可以使用多租户体系来向多个客户提供虚拟化资源,如虚拟机、存储和网络。每个客户都有自己的隔离环境,可以独立管理自己的资源,而不会影响其他客户。
-
软件即服务(SaaS):SaaS提供商可以使用多租户体系来向多个客户提供相同的应用程序和服务。每个客户都有自己的数据和配置,但共享相同的应用程序代码和基础设施。
-
共享经济平台:共享经济平台,如共享办公空间、共享汽车和共享住宿等,可以使用多租户体系来向多个用户提供服务。每个用户都有自己的账户和权限,但共享相同的物理资源。
-
物联网设备管理:在物联网环境中,许多设备需要连接到云平台进行管理和监控。使用多租户体系,可以将多个设备的数据隔离开来,确保数据安全和隐私。
-
在线游戏:在线游戏可以使用多租户体系来支持多个玩家同时玩游戏。每个玩家都有自己的角色和进度,但共享相同的游戏世界和资源。