spring boot实现多租户数据存储

 

 

背景

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

项目代码介绍

预备项目:实现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<String> 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> T unwrap(Class<T> 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<String, Object> 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);
    }
}

测试方式与原先一样

 

 

### Spring Boot 多租户架构实现 SaaS 平台的最佳实践 构建一个多租户的 SaaS 平台涉及技术选型、数据隔离策略以及业务逻辑的设计等多个方面。以下是关于如何使用 Spring Boot 实现多租户架构的一些最佳实践: #### 1. 数据库隔离方式的选择 在多租户系统中,数据库隔离是最核心的部分之一。通常有三种主要的数据隔离方法: - **单独数据库模式**:每个租户都有自己的独立数据库实例。这种方式能够最大程度上保证数据的安全性和隔离性,但也增加了部署和维护的成本[^4]。 - **共享数据库/分离Schema模式**:所有租户共用一个数据库,但是每个租户对应于自己独特的 Schema 或者表前缀来存储其专属数据。此方案平衡了性能与复杂度之间的关系,在实际项目中有较高的接受度。 - **共享数据库/共享表格模式**:所有的租户都在同一张表里保存他们的记录,并通过特定字段(比如 TenantId 列)区分哪些行属于哪个租户。这种方法简单易操作,适合小型应用或初期阶段的产品开发[^3]。 #### 2. 租户识别机制 为了正确处理来自不同租户请求,必须建立有效的租户身份验证流程。常见的做法是在每次HTTP请求头加入`X-Tenant-ID`这样的自定义头部参数作为标识符;或者是依据URL路径的不同部分动态解析当前会话所属的具体租户信息[^2]。 #### 3. 动态数据源切换配置 当采用“每租户独享DB”的设计方案时,则需要考虑如何灵活地加载各个连接池设置并按需调用相应DS对象完成CRUD动作。可以通过编写拦截器或者AOP切面程序捕获上述提到过的tenant id值进而决定应该激活哪一个DataSource Bean实例参与事务执行过程[^1]。 ```java @Configuration public class DataSourceConfig { @Bean(name="dataSourceRouter") public AbstractRoutingDataSource routingDataSource(){ Map<Object, Object> targetDataSources = new HashMap<>(); // 假设我们有两个租户对应的两个数据源 targetDataSources.put("tenant1", tenantOneDataSource()); targetDataSources.put("tenant2", tenantTwoDataSource()); AbstractRoutingDataSource dataSource = new CustomRoutingDataSource(); dataSource.setTargetDataSources(targetDataSources); return dataSource; } } ``` 以上代码片段展示了如何创建一个路由型的数据源bean用于支持多套物理db环境下的查询工作流管理。 #### 4. 安全保障措施 除了基本的功能外还需要特别关注安全性问题,防止跨租户攻击(Cross-Tenancy Attacks),即某个恶意用户试图获取不属于他的其他用户的敏感资料的情况发生。为此可以在ORM框架层面增加额外校验逻辑确保只有经过授权后的主体才能访问指定范围内的实体集合。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值