Springboot+Hibernate多租户的使⽤(转)
Spring boot + Hibernate 多租户的使⽤
多租户
多租户(Multi Tenancy/Tenant) 是⼀种软件架构,其定义是:在⼀台服务器上运⾏单个应⽤实例,它为多个租户提供服务。
概念是抽象的,但是理解起来并不困难,简单来说就是分组,举个例⼦:我们管理学校学⽣的时候,可以按照不同的范围来进⾏分组,⽐如我们可以按照学⽣个⼈为单位进⾏分组,也可以按照班级为单位进⾏分组,然后班级下⾯有很多的学⽣,也可以按照年级为单位进⾏分组,以学校为单位……这样的每⼀个分组的单位,都可以是我们概念⾥⾯说的⼀个租户。
但是这样不就和我们以前说的按照⾯向对象来分类是⼀样的吗?其实是差不多的,但是有着⼀些细节上的差别,⾸先多租户架构的概念是针对数据存储的,我们是⼀个数据服务提供商,假设我们给所有的学校提供服务,对于我们来说,分组是按照学校为单位的,⽽且学校与学校之间互相没有任何关系,也就说学校与学校之间是隔离的,对于不同学校的数据我们需要将它们隔离开来。这种数据的分组就是多租户架构要研究的问题。
当然这只是概念上的区别,在实际使⽤上和我们传统的分组并⽆太⼤差异。
多租户的三种模式
多租户的架构分为以下三种:
1. 独⽴数据库
2. 共享数据库,独⽴Schema
3. 共享数据库,独⽴Schema,共享数据表
注:在这个架构的概念⾥⾯,数据库指的是物理机器数据库,也就是我们的⼀部运⾏着数据库软件的计算机是⼀个物理数据库,Schema就是我们在数据库软件⾥⾯创建的“数据库”,实际上都是在同⼀个物理机器⾥⾯的,表就是表,⼀个简单的表
独⽴数据库是⼀个租户独享⼀个数据库实例,它提供了最强的分离度,租户的数据彼此物理不可见,备份与恢复都很灵活;
共享数据库、独⽴ Schema 将每个租户关联到同⼀个数据库的不同 Schema,租户间数据彼此逻辑不可见,上层应⽤程序的实现和独⽴数据库⼀样简单,但备份恢复稍显复杂;
最后⼀种模式则是租户数据在数据表级别实现共享,它提供了最低的成本,但引⼊了额外的编程复杂性(程序的数据访问需要⽤ tenantId 来区分不同租户),备份与恢复也更复杂。
这三种模式的特点可以⽤⼀张图来概括:
三种部署模式的异同
多租户模式选择
从上⾯的图我们可以看到,在成本上,独⽴数据库是最⾼的,毕竟我们⼀个租户就是⼀个物理机器,⽽且数据共享起来会⿇烦,涉及到跨物理机器的通信,但这种模式的优势体现在单个租户数据量庞⼤,⽽且有⾮常⼤的扩展需求,那么单个机器内的调整就⾮常容易,⽽且不会影响到其他的租户,因为它的隔离程度是最⾼的。
事实上,多租户模式的选择,主要是成本原因,对于多数场景⽽⾔,共享度越⾼,软硬件资源利⽤效率更好,成本更低。但同时也要解决好租户资源共享和隔离带来的安全与性能、扩展性等问题。毕竟,也有客户⽆法满意于将数据与其他租户放在共享资源中。Hibernate 多租户的使⽤
Mybatis 多租户的使⽤
⼀开始我也是使⽤Mybatis进⾏多租户的设计,但是事实上Mybatis本⾝是没有对多租户提供⽀持的,也就说我们如果使⽤Mybatis设计多租户的架构的话,那么我们就需要⼿动实现sql语句的拦截然后在执⾏具体sql语句之前执⾏use tenant_id的操作,拦截sql语句的⼀个⽐较简单的⽅式是通过spring aop在service层的操作⾥进⾏切⼊实现拦截。
实际上Hibernate也是这么⼲的,不过Hibernate在框架层⾯帮我们进⾏了sql语句拦截,不需要⾃⼰设计。
虽然最后我选择了Hibernate进⾏多租户的设计,但是这⾥也记录下Mybatis的设计思路,实现起来就简单了。
项⽬结构
可能与Github(地址在⽂章末尾)实际编码有点出⼊,因为我可能会修改,但⼤体相同。
主要⽬录及⽂件说明
config
⼀些设置⽂件,⼀开始我有⼀些设置⽂件的,但是后来去掉了,所以你可以忽略这个设置⽂件夹
ConstId
⽤来暂存租户ID TenantId的⼀个⽂件,没有特别的作⽤,通常情况下,这个租户ID是登陆的时候存在session⾥⾯的,然后读取也是从session⾥⾯读取,这⾥显然是我为了⽅便就随便⽤⼀个⽂件来存了
controller
顾名思义……
HelloController
dao
这个也不解释了,dao层
StudentDao
TenantInfoDao
entity
实体类……
Student
TenantInfo
这个是租户信息的实体类
service
Service层,只有⼀个StudentService是因为我嫌⿇烦就不多创建⼀个TenantInfoService了
StudentService
tenant
多租户相关的⽂件都在这⾥了,这个⽂件夹下的⽂件是重点!这些类的作⽤会在下⾯详细分析,这⾥就先不赘述了MultiTenantConnectionProviderImpl
MultiTenantIdentifierResolver
TenantDataSourceProvider
util
⼀些辅助的⼯具,⽅便操作⽤的(各个web项⽬都可以通⽤,⼤家可以参考)
JsonUtil
给Gson整了⼀个单例,不同到处new Gson()
Result
统⼀的返回结果格式,满⾜REST架构
ResultCode
统⼀的返回码,参照HTTP响应码
ResultGenerator
构造返回Result结果的⼯具类
CloudApplication.java
数据库结构和说明
⾸先在数据库⾥有三个Schema,其中cloud_config是存储租户信息的,class_1和class_2分别为我们预设的两个租户
cloud_config的tenant_info表结构
字段说明
id
主键
tenant_type
spring aop应用场景数据库类型,⽤于识别连接不同的数据库的时候设置驱动的字段,在我这个⼩Demo中没有⽤上
url
数据库连接URL
username
数据库连接⽤户名
password
数据库连接密码
tenant_id
租户ID
class_1和class_2的student表结构
代码
实际上需要设置的代码⾮常简单,但是⽹上的资料极其稀少,很多Demo项⽬都没有注释和说明,让我⾛了很多弯路,也是促使我写⼀个博客来说明这个多租户配置和使⽤的主要动⼒
application.properties
怎么配置开启Hibernate的多租户功能,⽹上各种配置形式都有,有两种形式,⼀种是写配置类,⼀种就是在application.properties⽂件直接配置,显然直接配置要⽐配置类简单太多了
# Database
spring.datasource.url=jdbc:postgresql://localhost:5432/cloud_config
spring.datasource.username=lanyuanxiaoyao
spring.datasource.password=
spring.datasource.driver-class-name=org.postgresql.Driver
# Hibernate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.multiTenancy=SCHEMA
spring.jpa.ant_identifier_ant.MultiTenantIdentifierResolver
spring.jpa.properties.hibernate.multi_tenant_connection_ant.MultiTenantConnectionProviderImpl
这就是所需要的所有相关配置(如果你有别的配置就另外加上就是了),其中Database配置⼀定要有,就是⼀定要有⼀个默认的配置才能启动Spring boot,这个不能省……这是⼀个坑。
关于Hibernate的⼏个配置项的说明
show-sql
这个也⽆关多租户的设置,只是在控制台显⽰Hibernate执⾏的sql语句,⽅便调试
hibernate.multiTenancy
选择多租户的模式,有四个参数:NONE,DATABASE,SCHEMA,DISCRIMINATOR,其中NONE就是默认没有模
式,DISCRIMINATOR会在Hibernate5⽀持,所以我们根据模式选择是独⽴数据库还是不独⽴数据库就可以了,我这⾥选择
SCHEMA,因为只有⼀台物理机器
租户ID解析器,简单来说就是这个设置指定的类负责每次执⾏sql语句的时候获取租户ID
hibernate.multi_tenant_connection_provider
这个设置指定的类负责按照租户ID来提供相应的数据源
配置后三个设置项的时候会没有⾃动提⽰,直接复制就⾏了,只要名字没错就ok,因为没有⾃动提⽰搞到我以为设置在这⾥是不⾏的tenant包
这⾥的三个类是全部和多租户相关的类,这⾥我连同导包的信息也⼀并贴上了,希望⼤家不要导错包,同名的包有不少
TenantDataSourceProvider
ity.TenantInfo;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author lanyuanxiaoyao
*/
public class TenantDataSourceProvider {
// 使⽤⼀个map来存储我们租户和对应的数据源,租户和数据源的信息就是从我们的tenant_info表中读出来
private static Map<String, DataSource> dataSourceMap = new HashMap<>();
/**
* 静态建⽴⼀个数据源,也就是我们的默认数据源,假如我们的访问信息⾥⾯没有指定tenantId,就使⽤默认数据源。    * 在我这⾥默认数据源是cloud_config,实际上你可以指向你们的公共信息的库,或者拦截这个操作返回错误。
*/
static {
DataSourceBuilder dataSourceBuilder = ate();
dataSourceBuilder.url("jdbc:postgresql://localhost:5432/cloud_config");
dataSourceBuilder.username("lanyuanxiaoyao");
dataSourceBuilder.password("");
dataSourceBuilder.driverClassName("org.postgresql.Driver");
dataSourceMap.put("Default", dataSourceBuilder.build());
}
// 根据传进来的tenantId决定返回的数据源
public static DataSource getTenantDataSource(String tenantId) {
if (ainsKey(tenantId)) {
System.out.println("GetDataSource:" + tenantId);
(tenantId);
} else {
System.out.println("GetDataSource:" + "Default");
("Default");
}
}
// 初始化的时候⽤于添加数据源的⽅法
public static void addDataSource(TenantInfo tenantInfo) {
DataSourceBuilder dataSourceBuilder = ate();
dataSourceBuilder.Url());
dataSourceBuilder.Username());
dataSourceBuilder.Password());
dataSourceBuilder.driverClassName("org.postgresql.Driver");
dataSourceMap.TenantId(), dataSourceBuilder.build());
}
}
MultiTenantConnectionProviderImpl