Spring + Hibernate多租户配置

介绍

多租户(Multi-tenancy)是一种软件架构,一个服务实例可以服务多个客户,每个客户叫一个租户。而这其中最关键的一部分就是各个租户的数据的分离。

针对这种情形,主要有三种策略,数据的隔离级别从高到低依次是:Database per Tenant, Shared Database, Separate Schema, Shared Database, Shared Schema:

Database per Tenant: 每一个tenant有它自己的数据库实例,并且是和其他tenant的数据库隔离的。

Shared Database, Separate Schema: 所有的tenant共享一个数据库,但是每个tenant被schema隔离,有自己的专属schema。因为在Mysql中database等同于schema,所以即为一个数据库实例不同的数据库。

Shared Database, Shared Schema: 所有的tennat共享数据库和表,但是每个表以一个列区分不同tenant的数据,比如company_id, origanization_id等。

一般情况下,实现多租户可以通过Spring Boot的方式或者Hibernate的方式来实现,因为我们项目是基于Hibernate,所以这里要介绍的也是通过Hibernate的方式。

 

请求流程

通常情况下,实现多租户,连接当前租户的数据库由以下几个步骤组成:

  1. 拦截请求,检查用户是否登录,如果没有重定向用户到登录页。
  2. 根据请求中的信息识别用户属于哪个tenant。识别用户所属tenant是基于默认的database或schema的,它有需要使用到的数据, 比如说当前用户的company_id对应的database或schema的数据或信息。
  3. 和请求用户所属的tenant的数据库或schema建立连接。

 

登录

第一步验证登录其实就是普通的执行验证的流程,比如Spring security或者Shiro等框架提供的登录功能,即只要保证用户在进入下一步之前是已登录状态就行。如果是可以匿名访问的url当然也是另当别论。

 

找到用户所属tenant

识别用户所属tenant可以通过Spring的拦截器来实现,根据请求头所带的信息来得到所属的tenant,然后存在一个ThreadLocal变量中。这样在接下来的其他处理中可以拿到当前的tenant。在请求结束后把ThreadLocal中的内容清除。

首先创建一个类来存储当前的Tenant。静态类变量currentTenant存储的就是当前tenant的标识符。InheritableThreadLocal使得当前线程创建的子线程也可以继承这个tenant的值。

class TenantContext {

  private TenantContext() {
  }

  private static final ThreadLocal<String> currentTenant = new InheritableThreadLocal<>();

  static String getCurrentTenant() {
    return currentTenant.get();
  }

  static void setCurrentTenant(final String tenant) {
    currentTenant.set(tenant);
  }

  static void clear() {
    currentTenant.remove();
  }
}

然后是Spring拦截器,用来拦截请求,并找到当前用户所属的tenant。我这里是找的company_id,因为我这里默认数据库里有一张表存储了company_id和tenant所属数据库的对应关系。因此根据company_id能够找到tenant的数据库名称(我这里使用的是一个数据库实例,多个database/schema - mysql数据库)。

public class TenantInterceptor extends HandlerInterceptorAdapter {

  @Override
  public boolean preHandle(
      final HttpServletRequest request, final HttpServletResponse response, final Object handler) {
    String bearerToken = request.getHeader("Authorization");

    if (StringUtils.isBlank(bearerToken)) {
      return true;
    }

    bearerToken = bearerToken.replace("Bearer", "").replace(" ", "");

		// 从jwt token中解析出company id
		String companyId = getCompanyId(barerToken);

    TenantContext.setCurrentTenant(companyId);
    return true;
  }

  @Override
  public void afterCompletion(
      final HttpServletRequest request,
      final HttpServletResponse response,
      final Object handler,
      @Nullable final Exception ex) {
    TenantContext.clear();
  }
}

 

连接到tenant所属数据库

为了使Hibernate支持多租户,需要实现两个接口,一个是CurrentTenantIdentifierResolver 用来得到tennat identifier,被下一个接口使用得到数据库连接,另一个是MultiTenantConnectionProvider用来得到数据库连接。

首先是实现接口CurrentTenantIdentifierResolver, 根据company id得到数据库(database/schema)的名称。

public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {

  private final DatabaseManager databaseManager;

  @Autowired
  public TenantIdentifierResolver(@Lazy final DatabaseManager databaseManager) {
    this.databaseManager = databaseManager;
  }

  @Override
  public String resolveCurrentTenantIdentifier() {

    if (StringUtils.isBlank(TenantContext.getCurrentTenant())) {
      return databaseManager.getDefaultSchemaName();
    }

    return databaseManager.getSchemaNameByCompanyId(TenantContext.getCurrentTenant());
  }

  @Override
  public boolean validateExistingCurrentSessions() {
    return true;
  }
}

然后实现另一个接口MultiTenantConnectionProvider来获取与释放数据库连接。

@Component
public class TenantConnectionProvider implements MultiTenantConnectionProvider {

  private static final long serialVersionUID = -1166976596388409766L;

  private final transient DatabaseManager databaseManager;

  private final transient DataSource defaultDataSource;

  @Autowired
  public TenantConnectionProvider(@Lazy final DatabaseManager databaseManager,
      final DataSource pactsafeDataSource) {
    this.databaseManager = databaseManager;
    defaultDataSource = pactsafeDataSource;
  }

  @Override
  public Connection getAnyConnection() throws SQLException {
    return defaultDataSource.getConnection();
  }

  @Override
  public void releaseAnyConnection(final Connection connection) throws SQLException {
    connection.close();
  }

  @Override
  public Connection getConnection(final String tenantIdentifier) throws SQLException {
    final Connection connection = getAnyConnection();
    connection.setCatalog(tenantIdentifier);
    connection.setSchema(tenantIdentifier);
    return connection;
  }

  @Override
  public void releaseConnection(final String tenantIdentifier, final Connection connection)
      throws SQLException {
    connection.setSchema(tenantIdentifier);
    connection.setCatalog(tenantIdentifier);
    releaseAnyConnection(connection);
  }

  @Override
  public boolean supportsAggressiveRelease() {
    return false;
  }

  @Override
  public boolean isUnwrappableAs(final Class unwrapType) {
    return false;
  }

  @Override
  public <T> T unwrap(final Class<T> unwrapType) {
    return null;
  }
}

最后需要加上Hibernate配置文件:

@Configuration
public class HibernateConfiguration {

  private final JpaProperties jpaProperties;

  @Autowired
  public HibernateConfiguration(final JpaProperties jpaProperties) {
    this.jpaProperties = jpaProperties;
  }

  @Bean
  JpaVendorAdapter jpaVendorAdapter() {
    return new HibernateJpaVendorAdapter();
  }

  @Bean
  LocalContainerEntityManagerFactoryBean entityManagerFactory(
      final DataSource dataSource,
      final MultiTenantConnectionProvider multiTenantConnectionProvider,
      final CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
    final Map<String, Object> newJpaProperties = new HashMap<>(jpaProperties.getProperties());
    newJpaProperties.put(MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
    newJpaProperties.put(
        MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
    newJpaProperties.put(
        MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver);
    newJpaProperties.put(
        IMPLICIT_NAMING_STRATEGY, SpringImplicitNamingStrategy.class.getName());
    newJpaProperties.put(
        PHYSICAL_NAMING_STRATEGY, SpringPhysicalNamingStrategy.class.getName());
    newJpaProperties.put(DIALECT, MySQL57Dialect.class.getName());

    final LocalContainerEntityManagerFactoryBean entityManagerFactoryBean =
        new LocalContainerEntityManagerFactoryBean();
    entityManagerFactoryBean.setDataSource(dataSource);
    entityManagerFactoryBean.setJpaPropertyMap(newJpaProperties);
    entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter());
    entityManagerFactoryBean.setPackagesToScan("your_package_here");
    entityManagerFactoryBean.setPersistenceUnitName("default");
    return entityManagerFactoryBean;
  }
}

然后Hibernate连接数据库的时候就会首先根据首先调用CurrentTenantIdentifierResolver获取tenant identifier,然后调用MultiTenantConnectionProvider来获取数据库连接。这样就能够连接到正确的数据库。

 

通过EntityManager来连接指定数据库

另外如果想连接指定的数据库,而不是当前的tenant的数据库,可以通过EntityManagerFactory提供的功能来实现,代码示例如下:

    final Session session = entityManagerFactory.unwrap(SessionFactory.class)
            .withOptions()
            .tenantIdentifier(schemaName)
            .openSession();
    final Transaction transaction = session.getTransaction();
    transaction.begin();
    session.save(entity);
    transaction.commit();
    session.close();
    return entity;

https://medium.com/swlh/multi-tenancy-implementation-using-spring-boot-hibernate-6a8e3ecb251a

https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#multitenacy

https://medium.com/innomizetech/dynamic-multi-database-application-with-spring-boot-7c61a743e914

https://www.baeldung.com/spring-abstract-routing-data-source

おすすめ

転載: blog.csdn.net/adolph09/article/details/106392820