博主和芋道源码作者及其官方开发团队无任何关联
一、概述
租户(Tenant)是系统中的一个逻辑隔离的单元,代表一个独立使用系统的组织(如企业、高校等),在多租户系统中,不同租户共享相同的应用程序和基础设施,但各自拥有独立的数据、配置、组织架构及用户等。
芋道是一个支持多租户的系统,对多租户功能的组件和框架封装的代码位于yudao-spring-boot-starter-biz-tenant模块中,对于读写数据库和Redis,消息队列中消息的生产消费以及定时任务派发,调用异步方法等都分别实现了租户隔离,实现原理都是利用线程ThreadLocal进行租户标识传递和线程内共享,处理租户业务的线程(例如WebApi的HTTP请求线程,定时任务执行线程,消息消费回调线程)开始执行时首先获取具体场景下的租户ID,存到当前线程的ThreadLocal中,后续基于该线程执行或调用的各种方法中如遇到读写数据库或Redis以及发送消息和调用异步方法的操作时,便能从ThreadLocal获取租户ID再执行进一步操作。
项目通过一个TenantContextHolder类来封装ThreadLocal进而实现不同场景下基于线程的租户隔离,为每个执行带有租户隔离逻辑代码的线程都绑定一个TENANT_ID对象来储存和共享当前场景的租户ID,同时还绑定了一个布尔类型的IGNORE用于标识当前线程即将要执行的代码是否需要处理租户。
只有深入正确理解TenantContextHolder中ThreadLocal的原理,才能真正理解多租户的实现原理- public class TenantContextHolder {
- /**
- * 当前租户编号
- */
- private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
- /**
- * 是否忽略租户
- */
- private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
- /**
- * 获得租户编号
- *
- * @return 租户编号
- */
- public static Long getTenantId() {
- return TENANT_ID.get();
- }
- /**
- * 获得租户编号。如果不存在,则抛出 NullPointerException 异常
- *
- * @return 租户编号
- */
- public static Long getRequiredTenantId() {
- Long tenantId = getTenantId();
- if (tenantId == null) {
- throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:"
- + DocumentEnum.TENANT.getUrl());
- }
- return tenantId;
- }
- public static void setTenantId(Long tenantId) {
- TENANT_ID.set(tenantId);
- }
- public static void setIgnore(Boolean ignore) {
- IGNORE.set(ignore);
- }
- /**
- * 当前是否忽略租户
- *
- * @return 是否忽略
- */
- public static boolean isIgnore() {
- return Boolean.TRUE.equals(IGNORE.get());
- }
- public static void clear() {
- TENANT_ID.remove();
- IGNORE.remove();
- }
- }
复制代码 多租户还需要考虑忽略租户和指定租户的情况:
调用某些方法时,租户应当被忽略,例如超级管理员获取系统全部数据、项目启动后获取全部数据去创建全局静态缓存等,因此该项目也提供了租户忽略的实现方案,对于某段需要忽略租户执行的代码,提供了忽略租户去执行某个代码块的公共方法,对于整个方法需要忽略租户的情况,则通过AOP处理自定义注解的方式,对某个方法标记忽略租户,该方法内执行的代码便不再对多租户的情况进行处理。
调用某些方法时,应当以指定的某个租户ID去执行,而不是采用当前登录用户的租户ID,例如超管新建了一个租户,并为新租户一并创建管理员用户以及基本的角色,菜单和权限等数据时,这些数据的租户ID应该是新建的租户的ID,针对这种情况,项目也实现了一个按照指定租户ID执行某个代码块的公共方法。
二、数据库的租户隔离
2.1 数据库的租户隔离实现方案
数据库中租户的隔离方案有三种:
- 库隔离:每个租户拥有独立的数据库实例
- 表隔离:每个租户建属于自己的一套数据表
- 记录隔离:表中使用租户标识字段(tenant_id)区分不同租户的数据
三种方案对比:
库隔离表隔离记录隔离隔离性最高较高最低性能高中低,受租户数量影响备份还原难度简单中困难硬件成本高中低维护成本高,开租户就要建库中低芋道采用了记录隔离的方式来实现数据库的租户隔离。
2.2 实现原理和源码解读
租户本质上也是一种特殊的数据权限,不同于数据权限的是对于涉及租户的表的增、删、改、查四种操作,都需要对SQL语句进行处理,实现原理是执行SQL前进行拦截,并获取要执行的SQL,然后解析SQL语句中的表,遇到需要租户隔离的表就要进行处理,对于查询、删除和更新的场景,就在现有的SQL条件中追加一个tenant_id = ?的条件,获取当前操作的用户或要执行的某种任务所属的租户ID赋值给tenant_id,对于添加操作,则是将tenant_id字段加入到INSERT列表中并赋值。
芋道采用MyBatis-Plus的插件拦截机制实现数据库的记录级别的租户隔离,这和数据权限的实现原理是完全一样的,实现租户隔离的插件是TenantLineInnerInterceptor,该类也像数据权限插件一样继承了用于解析和追加条件的BaseMultiTableInnerInterceptor类来实现表的解析和租户ID过滤条件的追加
TenantLineInnerInterceptor需要一个TenantLineHandler类型的租户处理器,TenantLineHandler是一个接口,用于给TenantLineInnerInterceptor判断某个表是否需要租户隔离,以及获取租户ID值表达式、租户字段名,我们需要实现这个接口并在回调方法中将这些信息封装好后返回。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |