JeeSite 4.x

Spring Boot 最好的快速开发平台

SaaS 架构、多租户架构使用文档说明书

SaaS 是 Software-as-a-Service(软件即服务)的简称,从技术角度上可称之为 “多租户技术或称多重租赁技术”。它与 “按需软件、应用服务提供商、托管软件” 所具有相似的含义。它是一种通过互联网提供软件的模式,厂商将应用软件统一部署在自己的服务器上,客户可以根据自己实际需求,通过互联网向厂商定购所需的应用软件服务,按定购的服务多少和时间长短向厂商支付费用,并通过互联网获得厂商提供的服务。用户不再需要购买软件,而改用向提供商租用基于Web的软件,来管理企业经营活动,且无需对软件进行维护,服务提供商会全权管理和维护软件,软件厂商在向客户提供互联网应用的同时,也提供软件的离线操作和本地数据存储,让用户随时随地都可以使用其定购的软件和服务。对于许多小型企业来说,SaaS是采用先进技术的最好途径,它消除了企业购买、构建和维护基础设施和应用程序的需要。

“多租户技术或称多重租赁技术” 是一种软件架构技术,是实现如何在多组织环境下共用相同的系统或程序组件,并且可确保各租户间数据的隔离性。在当下云计算时代,多租户技术在共用的数据中心以单一系统架构与服务提供多数客户端相同甚至可定制化的服务,并且仍可以保障客户的数据隔离。目前各种各样的云计算服务就是这类技术范畴。

实现方案及原理

多租户的数据隔离方案,不外乎以下三种方案(来自网络转载)分别是:

1)独立数据库

这是第一种方案,即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。

优点: 为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。

缺点: 增多了数据库的安装数量,随之带来维护成本和购置成本的增加。这种方案与传统的一个客户、一套数据、一套部署类似,差别只在于软件统一部署在运营商那里。如果面对的是银行、医院等需要非常高数据隔离级别的租户,可以选择这种模式,提高租用的定价。如果定价较低,产品走低价路线,这种方案一般对运营商来说是无法承受的。

2)共享数据库,隔离数据架构

这是第二种方案,即多个或所有租户共享 DataBase,但是每个租户一个 Schema(也可叫做一个user)。

优点: 为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;

缺点: 如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据;如果需要跨租户统计数据,存在一定困难。

3)共享数据库,共享数据架构

这是第三种方案,即租户共享同一个 DataBase、同一个 Schema,但在表中增加 corp_code 多租户的数据字段。这是共享程度最高、隔离级别最低、维护成本和购置最低的模式。

优点: 三种方案比较,第三种方案的维护和购置成本最低,跨租户统计方便,允许每个数据库支持的租户数量多。

缺点: 隔离级别最低,需要在设计开发时加大对安全的开发量;对单个租户的数据备份和恢复困难。

如果希望以最少的服务器为最多的租户提供服务,并且租户接受牺牲隔离级别换取降低成本,这种方案最适合。

JeeSite 是如何选择的?

通过上述三种方案分析,JeeSite 更倾向于共享数据库共享表方式,因为他最易用,租户之间数据共享更方便,维护运营成本最低。

若考虑租户数据非常大时,可考虑分区分表,以及可使用数据库中间件(TiDB、MyCat、Sharding-JDBC)来处理大数据量的分布式存储。

JeeSite 中如何使用多租户?

开启多租户,打开 application.yml 文件,设置如下:

# 用户相关
user:

  # 多租户模式(SAAS模式)(专业版)
  useCorpModel: true

通过上述的介绍,我们已了解 JeeSite 对多租户的实现方式是表结构数据共享方式,对于租户数据个性化的每张表,都需要有 corp_code 和 corp_name 租户字段,进行数据分离。

这两个字段不需要开发人员手动维护,在 java 代码中,只需要在实体类的 @Table 中增加 @Column(includeEntity=BaseEntity.class) 即可,有平台自动维护,不需要开发人员编写相应代码。

每个实体类都继承了 BaseEntity 基类,所以每个实体类都已经包含如下的两个 get 方法,用来自动获取当前租户的编码和名称,这两个方法会在将来的 insert 和 select 语句中自动体现:

public String getCorpCode() {
	if (StringUtils.isBlank(corpCode)){
		corpCode = CorpUtils.getCurrentCorpCode();
	}
	return corpCode;
}
public String getCorpName() {
	if (StringUtils.isBlank(corpName)){
		corpName = CorpUtils.getCurrentCorpName();
	}
	return corpName;
}

以 Office 举例,引入 includeEntity=BaseEntity.class 列,如下:

@Table(name="${_prefix}sys_office", alias="a", columns={
	@Column(includeEntity=BaseEntity.class),
})
public class Office extends TreeEntity<Office> {
}

再来看下 include 的 BaseEntity 实体,系统已经定义了 corp_code 和 corp_name 字段:

@Table(columns={
	@Column(name="corp_code", attrName="corpCode", label="租户代码", isUpdate=false),
	@Column(name="corp_name", attrName="corpName", label="租户名称", isUpdate=false, isQuery=false),
})
public abstract class BaseEntity<T> implements Serializable {
}

所以在 Office 中也自动的包含了 corp_code 和 corp_name 这两个字段。

有了这些配置:

  • 当执行 officeService.save() 或 insert() 保存时,系统会自动将 corp_code 和 corp_name 租户字段及当前租户信息,添加到 insert 语句里,不需要你再去通过 set 方式去维护它。

  • 当执行 officeService.findList() 等相关查询方法时,系统会自动将 corp_code 租户字段添加到 where 子句里,自动去过滤当前租户信息,不需要你再去通过 set 方式去设置当前租户。

  • 若查询时,想不想包含 corp_code 字段,你想进行查询全部租户的信息,你只需调用 entity.getSqlMap().getWhere().disableAutoAddCorpCodeWhere() 即可禁用 corp_code 条件的自动添加。

那如何新增一个租户?

用超级管理员(system)登录,进入菜单 “系统管理 -> 权限管理 -> 系统管理员” 可进行新增租户管理员,新增的租户管理员只可管理自己领域内的数据(如:部门、公司、用户、自定义的字典数据等)。

租户管理员关联角色

平台中引入了租户分配角色的功能,也就是说可以让不同的租户管理员,可以看到的菜单是不同的。

租户管理员平台也默认了一个角色,这个角色是所有租户管理员都有的,如下配置:

# 用户相关
user:
  # 系统管理员角色编号(客户方管理员使用的角色)
  corpAdminRoleCode: corpAdmin

如何获取租户名称列表?

User where = new User();
List<User> corpUserList = userService.findCorpList(where);
System.out.println(corpUserList);