spring boot实现多租户数据存储

 

 

背景

目前多租户数据存储模式主要有三种,分别是共享硬件隔离数据库实例、共享数据库实例隔离数据表、共享数据库实例共享数据表,这三种数据存储模式如下图所示。

spring boot实现多租户数据存储_第1张图片

项目代码介绍

预备项目:实现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#/

界面如下:

spring boot实现多租户数据存储_第2张图片

 点击multi-test进行访问,填充参数,获取响应

spring boot实现多租户数据存储_第3张图片

 

spring boot实现多租户数据存储_第4张图片

 

共享数据库实体集群隔离数据表模式实现

上述的方法中,只使用了一个数据库,这样租户的数量是有限的,如果希望使用多个数据库的话,也即是如下的示意图

spring boot实现多租户数据存储_第5张图片

 

实现的方案如下图所示

spring boot实现多租户数据存储_第6张图片

具体代码的实现策略是用缓存每个租户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);
    }
}

测试方式与原先一样

 

 

你可能感兴趣的:(spring boot实现多租户数据存储)