多租户实现原理

发布时间 2023-06-16 11:07:35作者: ZnPi

源码地址:

Gitee GitHub
后端 https://gitee.com/linjiabin100/pi-admin.git https://github.com/zengpi/pi-admin.git
前端 https://gitee.com/linjiabin100/pi-admin-web.git https://github.com/zengpi/pi-admin-web.git

概述

什么是多租户

多租户(Multi-tenancy)是一种软件架构模式,支持在同一应用程序或系统中同时为多个用户或组织提供服务。在多租户架构中,每个租户都被视为相对独立的客户。租户之间共享相同的应用程序实例、硬件资源和基础设施。然而,租户的数据和配置是相互隔离的,每个租户只能访问自己的数据和配置,彼此之间互不干扰。租户可以是个人用户、企业、组织或其他实体。

多租户架构在许多云计算服务中得到广泛应用,如 SaaS(软件即服务)和 PaaS(平台即服务)。它也适用于企业内部部署的软件系统,以支持在组织内部不同部门或团队之间共享资源和服务。

需要注意的是,多租户并非适用于所有情况。在某些场景下,单租户架构可能更加合适,例如对于需要高度定制化和独立性的客户。因此,在设计和选择软件架构时,需要根据具体需求和情况来决定是否采用多租户模式。

特性

  • 资源共享。由于多个租户共享同一实例,硬件资源和基础设施可以更有效地利用,从而降低了成本。
  • 简化管理和维护工作。通过集中管理单个软件实例,可以更轻松地进行软件部署、升级和维护。
  • 灵活性和可伸缩性。多租户架构可以根据客户需求进行扩展或收缩,从而实现更好的适应性和弹性。

实现思路

实现多租户功能需要考虑以下几个方面:

  • 数据隔离:每个租户需要有自己的数据。可以通过数据库或数据库的 schema(模式)或数据库表字段来进行隔离。在 pi-admin 中,多个租户共享相同的数据库,每个表都包含 tenant_id 字段,用于区分不同的租户。
  • 安全隔离:确保不同租户之间的数据和配置相互隔离,租户只能访问自己的数据。可以使用 MyBatis-Plus 的多租户插件通过为查询语句中的每个表添加 tenant_id 查询条件来实现这一点。
  • 租户识别:识别不同的租户,可以通过不同的方式进行,比如在请求中添加租户标识符(在请求头或请求参数中),或者通过子域名来识别租户。在 pi-admin 中,租户编码保存在 Spring Security 的 principal 中,以此来区分不同的租户。
  • 配置管理:根据租户的不同,可能需要对一些配置进行定制化。可以使用配置文件或数据库来管理租户特定的配置,然后在运行时根据租户进行加载和使用。
  • 单元测试和集成测试:在实现多租户功能时,需要编写相应的单元测试和集成测试,确保不同租户之间的隔离和功能正常工作。

功能模块

企业管理:管理企业信息,用户根据企业信息新增租户,一个企业对应一个租户。

套餐管理:维护租户所拥有的的菜单。

租户管理:维护租户信息,设置租户套餐,用户数量等。

企业管理

管理企业信息,用户根据企业信息新增租户,一个企业对应一个租户。

数据库表:sys_enterprise

字段名称 类型 允许空 默认值 字段描述
id bigint unsigned NO PK 主键
create_time datetime YES 创建时间
update_time datetime YES 更新时间
create_by varchar(16) YES 创建人
update_by varchar(16) YES 更新人
name varchar(128) NO UK 企业名称
name_en varchar(128) YES 英文名称
short_name varchar(128) YES 简称
usci varchar(128) YES 统一社会信用代码
registered_currency varchar(32) YES 注册币种
registered_capital varchar(16) YES 注册资本
legal_person varchar(16) YES 法人
establishing_time datetime YES 成立时间
business_nature varchar(256) YES 企业性质
industry_involved varchar(256) YES 所属行业
registered_address varchar(256) YES 注册地址
business_scope varchar(1000) YES 经营范围
staff_number varchar(32) YES 员工数
state varchar(16) YES 状态
deleted tinyint unsigned YES 0 是否删除(0:=未删除;null:=已删除)

细节

新增时企业名称不能重复。

当企业成为租户时无法删除。

套餐管理

维护租户所拥有的的菜单。

数据库表:sys_package

字段名称 类型 允许空 默认值 字段描述
id bigint unsigned NO PRI 主键
create_time datetime YES 创建时间
update_time datetime YES 修改时间
create_by varchar(16) YES 创建人
update_by varchar(16) YES 更新人
name varchar(64) NO UK 套餐名称
enabled tinyint unsigned YES 1 状态(0:=禁用; 1:=启用)
deleted tinyint unsigned YES 0 是否删除(0:=未删除;null:=已删除)
remark varchar(500) YES 备注

数据库表:sys_package_menu

字段名称 类型 允许空 默认值 字段描述
id bigint unsigned NO PRI 主键
package_id bigint unsigned NO FK 套餐标识
menu_id bigint unsigned NO FK 菜单标识

细节

当套餐已使用时无法删除。

套餐被禁用后无法查询,但不影响已经配置为该套餐的租户。

编辑套餐菜单时:

  • 当为套餐新增了菜单时,为套餐租户管理员角色新增菜单
  • 当为套餐减少了菜单时,为所有套餐租户角色删除指定菜单

租户管理

维护租户信息,设置租户套餐,用户数量等。

数据库表:sys_tenant

字段名称 类型 允许空 默认值 字段描述
id bigint unsigned NO 主键
create_time datetime YES 创建时间
update_time datetime YES 修改时间
create_by varchar(16) YES 创建人
update_by varchar(16) YES 更新人
tenant_code char(6) NO UK 租户编号
enterprise_id bigint unsigned NO FK 企业主键
enterprise_name varchar(64) NO 企业名称
admin_id bigint unsigned NO FK 租户管理员主键
contact varchar(16) NO 联系人
account varchar(16) NO UK 账号
phone varchar(32) YES 手机
email varchar(128) YES 邮箱
package_id bigint unsigned YES FK 租户套餐
expires datetime YES 到期时间
user_quantity bigint YES -1 用户数量(-1:=不限制)
enabled tinyint YES 1 状态(0:=禁用; 1:=启用)
deleted tinyint YES 0 是否删除(0:=未删除;null:=已删除)
remark varchar(500) YES 备注

新增租户

新增时后台做了一些事:

  • 生成 6 位租户编码;
  • 创建角色,角色名称为管理员,角色编码为 ADMIN,表示该角色为租户管理员,然后将角色与租户套餐菜单进行绑定;
  • 创建部门,部门名称为企业名称;
  • 创建租户管理员用户,联系人的名称为用户名称,将用户和角色、部门进行关联。
  • 新增租户

编辑租户

编辑时如果修改了套餐:

  • 当新套餐比原套餐新增了菜单时,为指定租户管理员角色新增菜单
  • 当新套餐比原套餐减少了菜单时,为所有租户角色删除指定菜单

细节

新增或编辑租户时校验手机号、邮箱是否正确。

删除租户同时删除该租户下的用户、角色、部门。

租户禁用功能目前无效。

获取租户

工具类 me.pi.admin.common.util.SecurityUtils 提供了获取租户的方法:

String tenantId = user.SecurityUtils.getTenantId();

前端可以在 pinia 中获取:

import useStore from "@/stores"

const { useUserStore } = useStore();

const tenantId = useUserStore.tenantId

指定表忽略多租户

开发中有一些表需要忽略多租户,常见的如多对多关系的中间表。可以在 mybatis-plus.tenant.ignore 中配置需要忽略的表名:

mybatis-plus:
  tenant:
    ignores:
      - sys_user_role
      - sys_role_dept

指定语句忽略多租户

对指定的语句忽略多租户,可以在 Mapper 文件的方法上标注 @InterceptorIgnore(tenantLine = "true") 注解:

@InterceptorIgnore(tenantLine = "true")
Set<TenantRoleMenuDTO> getTenantRoleMenu(@Param("tenantId") Long tenantId);

租户登录

使用租户账号登录时,就不能再像以前一样使用用户名来检索用户信息了。取而代之的是,在登录界面,如果输入的用户名关联了多个租户,需要用户自己决定使用哪个租户来进行登录。登录时后台根据用户名以及租户编码来检索用户信息。

需要注意的是,租户登录成功后查看菜单时,查看的是套餐关联的菜单,因为菜单不受多租户的约束,并且租户无法新增、修改或删除菜单。

总结

多租户是一种软件架构模式,支持在同一应用程序或系统中同时为多个用户或组织提供服务。实现多租户功能需要考虑数据隔离、安全隔离、租户识别和配置管理等。

在 pi-admin 中,多租户功能分成了三个模块,分别是企业管理、套餐管理和租户管理。它们需要注意的细节如下:

企业管理:

  • 新增时企业名称不能重复。
  • 当企业成为租户时无法删除。

套餐管理:

  • 当套餐已使用时无法删除。

  • 套餐被禁用后无法查询,但不影响已经配置为该套餐的租户。

  • 编辑套餐菜单时:

    • 当为套餐新增了菜单时,为套餐租户管理员角色新增菜单

    • 当为套餐减少了菜单时,为所有套餐租户角色删除指定菜单

租户管理:

  • 编辑时如果修改了套餐:

    • 当新套餐比原套餐新增了菜单时,为指定租户管理员角色新增菜单

    • 当新套餐比原套餐减少了菜单时,为所有租户角色删除指定菜单

  • 新增或编辑租户时校验手机号、邮箱是否正确。

  • 删除租户同时删除该租户下的用户、角色、部门。

需要注意的是,租户登录成功后查看菜单时,查看的是套餐关联的菜单,因为菜单不受多租户的约束,并且租户无法新增、修改或删除菜单。