目前多租户数据存储模式主要有三种,分别是共享硬件隔离数据库实例、共享数据库实例隔离数据表、共享数据库实例共享数据表,这三种数据存储模式如下图所示。
预备项目:实现swagger展示接口,以及对一个数据实体对象的读取操作;具体代码看:https://github.com/sysuKinthon/multi-tenant-database/tree/v1.0
共享数据库实例隔离数据表代码:https://github.com/sysuKinthon/multi-tenant-database/tree/v2.0
共享数据库集群隔离数据表代码:https://github.com/sysuKinthon/multi-tenant-database/tree/v3.1
实现思路是通过建立租户ID与数据库表的一对一对应关系,通过SpringMVC过滤器拦截租户的ID,根据租户的ID直接生成租户数据表的名称,然后直接获取到租户对应的数据表
1)加入multitenancy包,在包下添加TenantContext.java,使用ThreadLocal来保存每个租户请求对应的租户ID,代码如下
package org.bryson.singledatabasemultitenant.multitenancy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** 用ThreadLocal来保存租户信息*/
public class TenantContext {
private static Logger logger = LoggerFactory.getLogger(TenantContext.class);
private static ThreadLocal currentTenant = new ThreadLocal<>();
public static void setCurrentTenant(String tenant) {
logger.debug("Setting tenant to " + tenant);
currentTenant.set(tenant);
}
public static String getCurrentTenant() {
return currentTenant.get();
}
public static void clear() {
currentTenant.set(null);
}
}
2)在multitenancy包下添加拦截器TenantInterceptor.java,从租户请求中提取租户ID,并保存到ThreadLocal中;在config包下,添加MvcConfig注册TenantInterceptor拦截器
package org.bryson.singledatabasemultitenant.multitenancy;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class TenantInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String tenant = request.getParameter("tenantId");
TenantContext.setCurrentTenant(tenant);
return true;
}
@Override
public void postHandle(
HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
TenantContext.clear();
}
}
package org.bryson.singledatabasemultitenant.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Autowired
HandlerInterceptor tenantInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tenantInterceptor);
}
}
3)在multitenancy包下添加currentTenantIdentifierResolver.java,用于给Hibernate解析出当前的租户,代码如下
package org.bryson.singledatabasemultitenant.multitenancy;
import org.bryson.singledatabasemultitenant.constant.GlobalContext;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.springframework.stereotype.Component;
@Component
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {
@Override
public String resolveCurrentTenantIdentifier() {
String tenantId =TenantContext.getCurrentTenant();
// System.out.println("tenantId: " + tenantId);
if(tenantId != null) {
return tenantId;
}
return GlobalContext.DEFAULT_TENANT_ID;
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
4)在multitenancy包下加入MultiTenantConnectionProviderImpl,根据租户ID生成相应的数据库连接
package org.bryson.singledatabasemultitenant.multitenancy;
import org.bryson.singledatabasemultitenant.constant.GlobalContext;
import org.hibernate.HibernateException;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@Component
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {
@Autowired
private DataSource dataSource;
@Override
public Connection getAnyConnection() throws SQLException {
return dataSource.getConnection();
}
@Override
public void releaseAnyConnection(Connection connection) throws SQLException {
connection.close();
}
@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
final Connection connection = getAnyConnection();
try {
if (tenantIdentifier != null) {
connection.createStatement().execute("USE " + GlobalContext.DATABASE_SCHEMA_PREFIX + tenantIdentifier);
} else {
connection.createStatement().execute("USE " + GlobalContext.DATABASE_SCHEMA_PREFIX + GlobalContext.DEFAULT_TENANT_ID);
}
} catch ( SQLException e) {
throw new HibernateException(
"Could not alter JDBC connection to specified schema [" + GlobalContext.DATABASE_SCHEMA_PREFIX + tenantIdentifier + "]" + " " + e.toString(),
e
);
}
return connection;
}
@Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
try {
connection.createStatement().execute( "USE " + GlobalContext.DEFAULT_TENANT_ID );
}
catch ( SQLException e ) {
throw new HibernateException(
"Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]",
e
);
}
connection.close();
}
@Override
public boolean supportsAggressiveRelease() {
return true;
}
@Override
public boolean isUnwrappableAs(Class aClass) {
return false;
}
@Override
public T unwrap(Class aClass) {
return null;
}
}
5)在config包下,添加对Hibernate的配置
package org.bryson.singledatabasemultitenant.config;
import org.hibernate.MultiTenancyStrategy;
import org.hibernate.cfg.Environment;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 利用hibernate实现多租户数据库切换的主要原理
*/
@Configuration
public class HibernateConfig {
@Autowired
private JpaProperties jpaProperties;
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
MultiTenantConnectionProvider multiTenantConnectionProviderImpl,
CurrentTenantIdentifierResolver currentTenantIdentifierResolverImpl) {
Map properties = new HashMap<>();
properties.putAll(jpaProperties.getHibernateProperties(dataSource));
properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProviderImpl);
properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolverImpl);
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("org.bryson.singledatabasemultitenant");
em.setJpaVendorAdapter(jpaVendorAdapter());
em.setJpaPropertyMap(properties);
return em;
}
}
6)建立两个数据库,multitenant_0和multitenant_1,然后运行db.sql文件;
在两个数据表中插入数据,进行访问
访问:http://localhost:8082/swagger-ui.html#/
界面如下:
点击multi-test进行访问,填充参数,获取响应
上述的方法中,只使用了一个数据库,这样租户的数量是有限的,如果希望使用多个数据库的话,也即是如下的示意图
实现的方案如下图所示
具体代码的实现策略是用缓存每个租户ID与其数据库完整地址的映射
具体实现在上面的代码上进行如下的修改:
1)在multitenancy中加入两个类,TenantInfo.java和TenantDataSourcProvider.java,其中TenantInfo用于表示租户ID与数据表的关系,而TenantDataSourceProvider缓存了所有租户ID到数据库地址的关系
2)删除原本的MultiTenantConnectionProviderImpl.java,替换为如下内容
package org.bryson.singledatabasemultitenant.multitenancy;
import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
/**
* 这个类是Hibernate框架拦截sql语句并在执行sql语句之前更换数据源提供的类
* @author lanyuanxiaoyao
* @version 1.0
*/
@Component
public class MultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {
// 在没有提供tenantId的情况下返回默认数据源
@Override
protected DataSource selectAnyDataSource() {
return TenantDataSourceProvider.getTenantDataSource("Default");
}
// 提供了tenantId的话就根据ID来返回数据源
@Override
protected DataSource selectDataSource(String tenantIdentifier) {
System.out.println("tenantIdentifier: " + tenantIdentifier);
return TenantDataSourceProvider.getTenantDataSource(tenantIdentifier);
}
}
测试方式与原先一样