SpringMVC、c3p0、hibernate、JPA Application leaking Connections leads to Too Many Connections 错误

SpringMVC, c3p0, hibernate, JPA Application leaking Connections leads to Too Many Connections error

我已经在 Whosebug 上搜索并搜索了 4 个多小时来解决我的问题,阅读并尝试了解正在发生的事情,但我还没有找到与我的问题相关的解决方案,如果这听起来很抱歉像重复一样,我会尽力解释到底发生了什么,这样我就可以深入了解 c3p0 的内部工作原理。

我在 Tomcat 上有一个 SpringMVC Web 应用程序 运行,使用 Hibernate、JPA 和 C3P0 作为我的池资源。在页面重新加载时,应用程序会调用数据库以获取随机图像并将其显示在新页面上。该应用程序在大约 30 次左右的页面重新加载后运行良好,但最终总是崩溃,我必须重新启动 mysql 才能使应用程序再次运行,它会出现以下错误:

Apr 04, 2015 12:21:55 PM com.mchange.v2.resourcepool.BasicResourcePool$AcquireTask run WARNING: com.mchange.v2.resourcepool.BasicResourcePool$AcquireTask@634d8e3d -- Acquisition Attempt Failed!!! Clearing pending acquires. While trying to acquire a needed new resource, we failed to succeed more than the maximum number of allowed acquisition attempts (30). Last acquisition attempt exception: com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Data source rejected establishment of connection, message from server: "Too many connections" at sun.reflect.GeneratedConstructorAccessor101.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:526) at com.mysql.jdbc.Util.handleNewInstance(Util.java:411) at com.mysql.jdbc.Util.getInstance(Util.java:386) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1015) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:975) at com.mysql.jdbc.MysqlIO.doHandshake(MysqlIO.java:1114) at com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2493) at com.mysql.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:2526) at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2311) at com.mysql.jdbc.ConnectionImpl.(ConnectionImpl.java:834) at com.mysql.jdbc.JDBC4Connection.(JDBC4Connection.java:47) at sun.reflect.GeneratedConstructorAccessor43.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:526) at com.mysql.jdbc.Util.handleNewInstance(Util.java:411) at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:416) at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:347) at com.mchange.v2.c3p0.DriverManagerDataSource.getConnection(DriverManagerDataSource.java:135) at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource.getPooledConnection(WrapperConnectionPoolDataSource.java:182) at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource.getPooledConnection(WrapperConnectionPoolDataSource.java:171) at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPoolPooledConnectionResourcePoolManager.acquireResource(C3P0PooledConnectionPool.java:137) at com.mchange.v2.resourcepool.BasicResourcePool.doAcquire(BasicResourcePool.java:1014) at com.mchange.v2.resourcepool.BasicResourcePool.access0(BasicResourcePool.java:32) at com.mchange.v2.resourcepool.BasicResourcePool$AcquireTask.run(BasicResourcePool.java:1810) at com.mchange.v2.async.ThreadPerTaskAsynchronousRunner$TaskThread.run(ThreadPerTaskAsynchronousRunner.java:255)

这里有关于 files/configurations 的问题:

spring.xml:

<context:component-scan base-package="com.clathrop.infographyl.dao" />

<bean id="entityManagerFactory"
      class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceUnitName" value="infographylPU" />
    <property name="dataSource" ref="dataSource" />
</bean>

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
    <!-- Connection properties -->
    <property name="driverClass" value="com.mysql.jdbc.Driver" />
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/infographyl_db" />
    <property name="user" value="user" />
    <property name="password" value="passwd" />
    <!-- Pool properties -->
    <property name="minPoolSize" value="5" />
    <property name="maxPoolSize" value="20" />
    <property name="acquireIncrement" value="1" />
    <property name="maxStatements" value="0" />
    <property name="idleConnectionTestPeriod" value="3000" />
    <property name="loginTimeout" value="300" />
</bean>

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>

persistence.xml:

<persistence-unit name="infographylPU" transaction-type="RESOURCE_LOCAL">
    <class>com.clathrop.infographyl.model.Infographic</class>
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
        <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>
    </properties>
</persistence-unit>

这是一个基本查询的样子,在这个 class 我有一个 EntityManager 作为实例变量,我在每个查询完成后关闭()entityManager 但它对数量没有影响在数据库中建立的连接。

InfographicDaoImpl.java:

@Repository
public class InfographicDaoImpl implements InfographicDao{

@PersistenceContext
private EntityManager entityManager;

@Override
@Transactional
public void insertInfographic(Infographic infographic){
    try{
        entityManager.persist(infographic);
    } catch (Exception e){
        e.printStackTrace();
    } finally {
        entityManager.close();
    }

}

@Override
public List<Infographic> findAllInfographics(){
    try{
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Infographic> cq = builder.createQuery(Infographic.class);

        Root<Infographic> root = cq.from(Infographic.class);
        cq.select(root);
        List<Infographic> igList = entityManager.createQuery(cq).getResultList();
        return igList;
    } catch (Exception e){
        e.printStackTrace();
        return null;
    } finally {
        entityManager.close();
    }
}

@Override
public Infographic getRandomInfographic(Integer tableSize){
    Random rand = new Random();
    int randomIndex = rand.nextInt((tableSize-1)+1) + 1;

    try{
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Infographic> cq = builder.createQuery(Infographic.class);

        Root<Infographic> root = cq.from(Infographic.class);
        cq.select(root);
        cq.where(builder.equal(root.<Integer>get("id"), randomIndex));
        Infographic randomIg = entityManager.createQuery(cq).getSingleResult();
        return randomIg;
    } catch (Exception e){
        e.printStackTrace();
        return null;
    } finally {
        entityManager.close();
    }


}

@Override
public Integer getRowCount(){
    try{
        Number result = (Number) entityManager.createNativeQuery("Select count(id) from infographics").getSingleResult();
        return result.intValue();
    } catch (Exception e){
        e.printStackTrace();
        return null;
    } finally {
        entityManager.close();
    }
}

@Override
public List<Infographic> listInfographics(Integer startIndex, Integer pageSize){
    List<Infographic> igList = new ArrayList<Infographic>();

    String sStartIndex = Integer.toString(startIndex);
    String sPageSize = Integer.toString(pageSize);

    try{
        List list = entityManager.createNativeQuery("Select * from infographics limit " + sStartIndex + ", " + sPageSize).getResultList();
        for(Object ig : list){
            igList.add((Infographic) ig);
        }
        return igList;
    } catch(Exception e){
        e.printStackTrace();
        return null;
    } finally {
        entityManager.close();
    }


}

}

此时我对我的应用程序中发生的事情有几个问题。我的理解是 c3p0 正在处理与数据库建立的连接,我认为通过池连接,建立的连接将被重用,但是当我查看 [=] 中的进程列表时,我得到了越来越多的连接列表51=]。连接数不断增长,直到应用程序最终抛出上面显示的连接过多警告。我的问题是为什么 c3p0 不重用连接?我是否在某处缺少配置以告诉它重用预先存在的连接?为什么 entityManager.close() 貌似没有影响?如果我误解了如何使用 hibernate 和 c3p0,请告诉我我遗漏了什么。非常感谢任何帮助,提前致谢。

更新:

SEVERE: The web application [/infographyl] appears to have started a thread named [com.mchange.v2.async.ThreadPoolAsynchronousRunner$PoolThread-#0] but has failed to stop it. This is very likely to create a memory leak.

我已将问题缩小为 tomcat 启动并部署有问题的应用程序时识别到的内存泄漏。我现在的问题是试图找出导致问题的可能配置(或缺少配置)。

我目前的解决方法是设置 my.cnf 中的 wait_timeout 以杀死超过 5 秒的 processes/connections,这目前看来还可以,但这是不可持续的解决方案,我想知道关闭连接的正确方法。

更新 2: com.mchange.* 信息日志:

Apr 05, 2015 10:57:30 PM org.hibernate.service.jdbc.connections.internal.ConnectionProviderInitiator instantiateExplicitConnectionProvider INFO: HHH000130: Instantiating explicit connection provider: org.hibernate.ejb.connection.InjectedDataSourceConnectionProvider Apr 05, 2015 10:57:30 PM com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource getPoolManager INFO: Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 1, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, dataSourceName -> z8kfsx98137ghpr10fde73|6aa74262, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, identityToken -> z8kfsx98137ghpr10fde73|6aa74262, idleConnectionTestPeriod -> 3000, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://localhost:3306/infographyl_db, lastAcquisitionFailureDefaultUser -> null, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 20, maxStatements -> 0, maxStatementsPerConnection -> 0, minPoolSize -> 5, numHelperThreads -> 3, numThreadsAwaitingCheckoutDefaultUser -> 0, preferredTestQuery -> null, properties -> {user=******, password=******}, propertyCycle -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, usesTraditionalReflectiveProxies -> false ]

我认为,某些数据库操作会导致异常。在这种情况下,会抛出 Exception 并且 EntityManager 不会按预期关闭。通常你的数据库操作应该看起来像

        EntityManager em=getEntityManagerSomehow();
        try{
            do your stuff with db here
        }catch(Exception ex){ 
            handle exception here eg. log or rethrow.

          }finally{
           em.close();  // always close entity manager even if exception occures.
       }

在我看来,如果您像上图那样重构代码,问题就会得到解决。

我找到了解决我的问题的方法,但我不太确定这是最好的方法。

我通过以下方式将 my.cnf 中的 wait_timeout 设置为 5 秒:

[mysqld]
wait_timeout=5

这似乎可以有效地破坏 entityManager 正在创建的休眠进程,并允许我 运行 应用程序长时间使用而无需重新启动 mysql。

我现在的问题是,这真的是处理由 c3p0 + EntityManager 创建的连接的最好且唯一的方法吗?为什么 c3p0 不破坏连接或自行删除它们?这是故意的吗?任何澄清或讨论表示赞赏。谢谢大家:)

因此,从您所描述的一切来看,您的应用程序似乎正在创建然后放弃多个 c3p0 池。您不会遇到单个池耗尽(应用程序冻结)的常见症状。相反,您的应用程序打开的连接数多于 maxPoolSize,然后在达到服务器端连接限制时失败。除非您的服务器东西 ~20 连接太多,否则您可能会创建多个池。设置 wait_timeout 隐藏了问题,因为被放弃的数据源的连接会自动关闭(),但这不是一个好的解决方案。如果您为每个客户端制作新的数据源,您将大大减慢而不是加速您的应用程序,并且如果这些数据源没有关闭()ed(看起来它们不是,或者您不会累积打开的连接) ,您将造成线程和内存泄漏。

所以。

首先,您如何记录事情?请确保 com.mchange.* 类 已记录在 INFO 中,并检查查找池启动消息。 c3p0 在 DataSource 初始化时在 INFO 转储一个大型池配置消息。确保您至少看到其中一条消息。你见过他们很多次吗?那就是问题所在

如果我是对的,并且您正在打开然后放弃多个 c3p0 数据源,那么下一个问题就是为什么。应用程序中的池数据源嵌入在一个 EntityManagerFactory 对象中,在应用程序的生命周期中应该只有一个。 Spring 使事情看起来简单,但它隐藏了 how/when 事物构造、破坏等的细节。我认为您可能需要回答的关键问题是 why/whether Spring正在创建多个 EntityManagerFactory 实例(或在单个 EntityManagerFactory 中多次重新创建 c3p0 DataSource)。

p.s。 c3p0 不提供 "loginTimeout" 配置参数。