【问题标题】:Springboot Multi-tenant with MultiTenantConnectionProvider always throw org.apache.tomcat.jdbc.pool.PoolExhaustedException:带有 MultiTenantConnectionProvider 的 Springboot 多租户总是抛出 org.apache.tomcat.jdbc.pool.PoolExhaustedException:
【发布时间】:2019-06-14 04:55:44
【问题描述】:

我已经开始转换我现有的 Spring Boot(1.5.4.RELEASE) 应用程序以使用多租户功能。它是基于模式的多租户解决方案并基于 mysql。 正如下面建议的休眠文档

https://docs.jboss.org/hibernate/orm/4.2/devguide/en-US/html/ch16.html

我已经实现了 MultiTenantConnectionProvider 和 CurrentTenantIdentifierResolver 接口,并且工作正常。

package com.ifi.aws.tenant.config.hibernate;

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 com.ifi.aws.tenant.entity.TenantContext;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

@Component
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {

  private static final long serialVersionUID = 6246085840652870138L;

  @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 {
    connection.createStatement().execute( "USE " + tenantIdentifier );
  }
  catch ( SQLException e ) {
    throw new HibernateException(
        "Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]",
      e
      );
  }
  return connection;
}

@Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
  try {
    connection.createStatement().execute( "USE " + TenantContext.DEFAULT_TENANT );
  }
  catch ( SQLException e ) {
    throw new HibernateException(
        "Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]",
      e
      );
  }
  connection.close();
}

@SuppressWarnings("rawtypes")
@Override
public boolean isUnwrappableAs(Class unwrapType) {
  return false;
}

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

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

}

package com.ifi.aws.tenant.config.hibernate;

import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.springframework.context.annotation.Configuration;

import com.ifi.aws.tenant.entity.TenantContext;

@Configuration
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {

  @Override
  public String resolveCurrentTenantIdentifier() {


     String tenantId = TenantContext.getTenantSchema();

    //System.out.println("------------------ resolveCurrentTenantIdentifier = " + tenantId);
    if (tenantId != null) {
        return tenantId;
    }
    return TenantContext.DEFAULT_TENANT;
}
@Override
public boolean validateExistingCurrentSessions() {
    return true;
}
}

然后下面是我的休眠配置

package com.ifi.aws.tenant.config.hibernate;

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 java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

@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("com.ifi.aws");
    em.setJpaVendorAdapter(jpaVendorAdapter());
    em.setJpaPropertyMap(properties);
   return em;
  }
}

但有时系统会因以下错误而崩溃

Springboot Multi-tenant with MultiTenantConnectionProvider always throw org.apache.tomcat.jdbc.pool.PoolExhaustedException: [http-nio-8086-exec-2] Timeout: Pool empty. Unable to fetch a connection in 30 seconds, none available[size:100; busy:100; idle:0; lastwait:30000].

我在这个网站上做了一些阅读,并在以下问题中发现了完全相同的问题。

Spring Boot: Apache derby pool empty. Unable to fetch a connection in 30 seconds Tomcat Connection Pool Exhasuted

他们建议的修复方法之一是添加以下配置

spring.datasource.tomcat.max-active=100
spring.datasource.tomcat.max-idle=8
spring.datasource.tomcat.min-idle=8

但我仍然收到相同的错误,我调试代码并发现它在每次执行数据库调用后关闭连接。你们有什么想法吗?

编辑

昨天我发现 API 根本没有关闭任何连接。我写了一个简单的实用程序来检查连接状态,如下所示

    @Autowired
private DataSource ds;

@Before("execution(* com.ifi.aws.*.dao.impl.springData.*.*(..))")
public void logBeforeConnection(JoinPoint jp) throws Throwable {
    logDataSourceInfos("Before", jp);
}

@After("execution(* com.ifi.aws.*.dao.impl.springData.*.*(..)) ")
public void logAfterConnection(JoinPoint jp) throws Throwable {
    logDataSourceInfos("After", jp);
}

public void logDataSourceInfos(final String time, final JoinPoint jp) {
    final String method = String.format("%s:%s", jp.getTarget().getClass().getName(), jp.getSignature().getName());
    logger.debug("--------------------------------------------------------------------------");
    logger.debug(String.format("%s %s: number of connections in use by the application (active): %d.", time, method, ds.getNumActive()));
    logger.debug(String.format("%s %s: the number of established but idle connections: %d.", time, method, ds.getNumIdle()));
    logger.debug(String.format("%s %s: number of threads waiting for a connection: %d.", time, method, ds.getWaitCount()));
}

}

这表明活跃连接在持续增长。

Before com.sun.proxy.$Proxy127:findOne: number of connections in use by the application (active): 21.
Before com.sun.proxy.$Proxy127:findOne: the number of established but idle connections: 0.
Before com.sun.proxy.$Proxy127:findOne: number of threads waiting for a connection: 0

-----------------

After com.sun.proxy.$Proxy127:findOne: number of connections in use by the application (active): 21.
After com.sun.proxy.$Proxy127:findOne: the number of established but idle connections: 0.
After com.sun.proxy.$Proxy127:findOne: number of threads waiting for a connection: 0.
o.h.e.t.i.TransactionImpl                : committing

-------------------

Before com.sun.proxy.$Proxy127:findOne: number of connections in use by the application (active): 21.
Before com.sun.proxy.$Proxy127:findOne: the number of established but idle connections: 0.
Before com.sun.proxy.$Proxy127:findOne: number of threads waiting for a connection: 0

-----------------

After com.sun.proxy.$Proxy127:findOne: number of connections in use by the application (active): 22.
After com.sun.proxy.$Proxy127:findOne: the number of established but idle connections: 0.
After com.sun.proxy.$Proxy127:findOne: number of threads waiting for a connection: 0.
o.h.e.t.i.TransactionImpl                : committing

-------------------

但是在我的本地环境中它非常好,并且可以正确关闭连接。我的测试环境部署在 AWS t2 windows 实例中,此 API 部署为 Spring Boot jar 文件,并在同一 t2 实例中安装了 MYSQL 服务器。我能看到的唯一区别是操作系统版本,可能是一些 MYSQL 服务器配置

编辑

我按照@xerx593 的说明解决了这个问题

问题在于supportsAggressiveRelease = true,我按照@xerx593 的建议将其更改为false。但是我仍然想知道它是如何在我的本地环境中而不是在测试环境中工作的。根据休眠文档,它说“此连接提供程序是否支持积极释放 JDBC 连接并稍后重新获取这些连接(如果需要)?”。测试环境和本地环境配置一样,是操作系统版本还是mysql配置的结果?

谢谢, 凯伦

【问题讨论】:

  • MultiTenantConnectionProvider 可能是扔东西的人(他在前门吓了一跳),但连接泄漏可能在任何地方......或者它不是连接泄漏,但你的应用程序真的很频繁。 (你试过max-active=1000吗?)..什么是“不时”..?
  • @xerx593 感谢您的回复。有时意味着当我进行测试时它崩溃了,但我看不到任何模式。但是昨天我发现它根本没有关闭任何连接。如果我们设置 max-active=1000,它将在 1000 次数据库调用后崩溃。我做了一个小实用程序来检查连接,这个 api 在我的本地环境中正确关闭了连接,但是在 AWS t2 micto windows 实例上的暂存环境中,它根本没有关闭任何连接。请注意,MYSQL 服务器也在该 t2 实例中本地安装。更新了上面的描述
  • ...所以(对我来说)只是随机猜测: 1. DEFAULT_TENANT 在数据库上(关注)吗? 2.“尝试”将connection.close()封装成finally块(MTPImpl) 3.“尝试”supportsAggressiveRelease = false
  • @xerx593,谢谢。问题在于supportsAggressiveRelease = true,我按照您的建议将其更改为false。但是我仍然想知道它是如何在我的本地环境中而不是在测试环境中工作的。根据休眠文档,它说“此连接提供程序是否支持积极释放 JDBC 连接并稍后重新获取这些连接(如果需要)?”。测试环境和本地环境配置一样,是操作系统版本还是mysql配置的结果?
  • 但我不得不“猜测”它 3.(共 3 个),嗯!? -> 墨菲定律!? :) 很高兴,我们“修复”了它……但要真正“理解”(您的环境中的差异),我们需要更多信息/您必须进行比较! ;)

标签: java hibernate spring-boot multi-tenant


【解决方案1】:

通过“蛮力”,我们发现问题出在 supportsAggressiveRelease 标志中,当设置 (= true) 时,在 DEV 环境中显示没有问题,但会导致您的 AWS 实例出现问题。

解决方案:

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

为什么这个环境不支持“积极发布”,是你环境的配置/性质...

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-04-07
    • 1970-01-01
    • 1970-01-01
    • 2021-11-13
    • 2020-10-19
    • 1970-01-01
    • 2022-01-22
    • 1970-01-01
    相关资源
    最近更新 更多