【问题标题】:c3p0 DataSource monitor deadlock - all threads hang - how to fixc3p0 数据源监视器死锁 - 所有线程挂起 - 如何修复
【发布时间】:2015-09-10 11:13:15
【问题描述】:

我们有一个基于 Spring 的应用程序,最近我们开始投入生产。我们使用的是 Spring @Controller,它最终会命中使用 JDBCTemplate 的 DAO。它正在使用 c3p0 的 ComboPooledDataSource

在负载增加时(例如 150 个并发用户),应用程序为所有用户挂起 - DataSource 被某些东西锁定 - 在线程转储上,有大约 200 个线程说 - 显然 DataSource 已死锁。

"http-bio-8080-exec-440" - Thread t@878
java.lang.Thread.State: WAITING
at java.lang.Object.wait(Native Method)
- waiting on <146d984e> (a com.mchange.v2.resourcepool.BasicResourcePool)
at com.mchange.v2.resourcepool.BasicResourcePool.awaitAvailable(BasicResourcePool.java:1418)
at com.mchange.v2.resourcepool.BasicResourcePool.prelimCheckoutResource(BasicResourcePool.java:606)
at com.mchange.v2.resourcepool.BasicResourcePool.checkoutResource(BasicResourcePool.java:526)
at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutAndMarkConnectionInUse(C3P0PooledConnectionPool.java:756)
at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:683)
at com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource.getConnection(AbstractPoolBackedDataSource.java:140)
at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:111)
at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:77)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:573)
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:637)
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:666)
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:674)
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:718)

此后,除非重新启动,否则应用程序将无法使用。发生这种情况时,DBA 团队没有观察到数据库上的任何负载。

当时 c3p0 是这样配置的:

app_en.driverClass=com.mysql.jdbc.Driver
app_en.user=tapp_en
app_en.password=tapp_en
app_en.jdbcUrl=jdbc:mysql://10.10.0.102:3306/tapp_en?useUnicode=true&characterEncoding=utf-8&autoReconnect=true

app_en.acquireIncrement=5
app_en.maxIdleTime=3600
app_en.maxIdleTimeExcessConnections=300
app_en.unreturnedConnectionTimeout=3600
app_en.numHelperThreads=6
app_en.minPoolSize=20
app_en.maxPoolSize=100
app_en.idleConnectionTestPeriod=120
app_en.testConnectionOnCheckin=true

之后,我将 c3p0 的配置更改如下 - 并为 com.mchange.v2.c3p0 包启用了 DEBUG 日志记录:

app_en.driverClass=com.mysql.jdbc.Driver
app_en.user=tapp_en
app_en.password=tapp_en
app_en.jdbcUrl=jdbc:mysql://10.10.0.102:3306/tapp_en?    useUnicode=true&characterEncoding=utf-8&autoReconnect=true

app_en.acquireIncrement=5
app_en.maxIdleTime=180
app_en.maxIdleTimeExcessConnections=60
app_en.unreturnedConnectionTimeout=30
app_en.checkoutTimeout=10000
app_en.numHelperThreads=12
app_en.debugUnreturnedConnectionStackTraces=true
app_en.initialPoolSize=10
app_en.maxPoolSize=100
app_en.idleConnectionTestPeriod=120
app_en.preferredTestQuery="select 1 from tbl_users"

有了这个配置,我再次运行负载测试,应用程序仍然挂起......虽然线程在无法获得与数据库的连接后恢复。尽管线程恢复与以前的配置不同,但对于太多用户来说,游戏还是挂起 - 所以他们不得不重新启动他们的客户端。 尽管启用了所有日志记录,但 c3p0 日志不会记录任何死锁消息。我看到的错误信息就是:

[06/24/2015 12:20:54] [C3P0PooledConnectionPoolManager[identityToken->1oed6dl9a9ak8qsgqfvdu|4d6145af]-HelperThread-#10] DEBUG NewPooledConnection  - com.mchange.v2.c3p0.impl.NewPooledConnection@7f0bc55a closed by a client.
java.lang.Exception: DEBUG -- CLOSE BY CLIENT STACK TRACE
at com.mchange.v2.c3p0.impl.NewPooledConnection.close(NewPooledConnection.java:659)
at com.mchange.v2.c3p0.impl.NewPooledConnection.closeMaybeCheckedOut(NewPooledConnection.java:255)
at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool$1PooledConnectionResourcePoolManager.destroyResource(C3P0PooledConnectionPool.java:621)
at com.mchange.v2.resourcepool.BasicResourcePool$1DestroyResourceTask.run(BasicResourcePool.java:1024)
at com.mchange.v2.async.ThreadPoolAsynchronousRunner$PoolThread.run(ThreadPoolAsynchronousRunner.java:696)

应用程序中没有任何事务,我们也没有使用任何 TransactionManager 或 TransactionTemplate。我想知道这是否可能是使用的框架中的某种错误,或者配置错误。这些是使用的相关框架:

c3p0-0.9.5-pre8
mysql-connector-java-5.1.24
spring-core-3.2.1.RELEASE
spring-web-3.2.1.RELEASE
mchange-commons-java-0.2.7

我们非常感谢任何帮助,因为这阻碍了我们发布产品的努力。

附言编辑:这是数据源的配置:

<bean id="app_en_DataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
    destroy-method="close">
    <property name="driverClass" value="${app_en.driverClass}" />
    <property name="jdbcUrl" value="${app_en.jdbcUrl}" />
    <property name="user" value="${app_en.user}" />
    <property name="password" value="${app_en.password}" />

    <property name="acquireIncrement" value="${app_en.acquireIncrement}"></property>
    <property name="maxIdleTime" value="${app_en.maxIdleTime}"></property>
    <property name="maxIdleTimeExcessConnections" value="${app_en.maxIdleTimeExcessConnections}"></property>
    <property name="unreturnedConnectionTimeout" value="${app_en.unreturnedConnectionTimeout}"></property>
    <property name="checkoutTimeout" value="${app_en.checkoutTimeout}"></property>
    <property name="numHelperThreads" value="${app_en.numHelperThreads}"></property>
    <property name="debugUnreturnedConnectionStackTraces" value="${app_en.debugUnreturnedConnectionStackTraces}"></property>
    <property name="initialPoolSize" value="${app_en.initialPoolSize}"></property>
    <property name="maxPoolSize" value="${app_en.maxPoolSize}"></property>
    <property name="idleConnectionTestPeriod" value="${app_en.idleConnectionTestPeriod}"></property>
    <property name="preferredTestQuery" value="${app_en.preferredTestQuery}"></property>
</bean>

下面是应用程序中的一些代码,它没有直接使用 jdbcTemplate。没有其他的,其他的都是jdbcTemplate.update,jdbcTemplate.query:

    Connection conn = null;
    ResultSet getItemsRS = null;

    try {
        JdbcTemplate jdbcTemplate = getJdbcTemplate(database);

        conn = jdbcTemplate.getDataSource().getConnection();

        UserItems items;

        if (!action.areItemsNew()) {

            conn.setAutoCommit(false);
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

            PreparedStatement getItemsPS = conn.prepareStatement("select * from tbl_items where ownerId = ? for update",
                    ResultSet.TYPE_FORWARD_ONLY,
                    ResultSet.CONCUR_UPDATABLE);
            getItemsPS.setLong(1, userId);

            getItemsRS = getItemsPS.executeQuery();
            getItemsRS.next();

            items = new UserItemsRowMapper().mapRow(getItemsRS, getItemsRS.getRow());
        } else {
            items = new UserItems();
        }

        action.doUserItemsAction(items);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(items.getItemContainers());
        oos.close();
        byte[] data = baos.toByteArray();
        Blob blob = conn.createBlob();
        blob.setBytes(1, data);

        if (!action.areItemsNew()) {
            getItemsRS.updateBlob("data", blob);
            getItemsRS.updateRow();
        } else {
            jdbcTemplate.update("insert into tbl_items(ownerId,data) values(?,?)", userId, data);
        }

    } catch (Exception e) {
        logger.error(e);
        throw new RuntimeException(e);
    } finally {
        if (!action.areItemsNew()) {
            try {
                conn.commit();
                conn.close();
            } catch (SQLException e) {
                logger.error(e);
                throw new RuntimeException(e);
            }
        }
    }

这段代码的原因是我想在用户的项目被上面写的这个操作action.doUserItemsAction(items)更新之前阻止读/写。

【问题讨论】:

  • 抱歉,但这些链接并没有让我更深入地了解我已经遇到的问题......
  • 代码和配置在哪里?另外我建议使用另一个池而不是 C3P0,例如 HikariCP
  • @M.Deinum 代码太多,无法在此处发布。它基本上是在做jdbcTemplate.query、jdbcTemplate.update。只有一件事完全不同,它是读取、锁定、写入和释放 blob,直接使用 DataSource,autoCommit=false 并选择更新。如果你愿意,我可以把它贴在这里。其他的只是琐碎的 CRUD 操作
  • 如果您要进行更新,您将需要一个事务管理器,尤其是在您进行多次修改时,否则您可能会使数据库处于不一致的状态。此外,blob 的编写也很有趣,因为这看起来像是您自己在搞乱连接。此外,我不希望所有代码至少可能导致问题(例如,您正在使用/获取普通连接的代码)和您使用的配置。

标签: mysql jdbc spring-jdbc c3p0 spring-web


【解决方案1】:

您拥有的代码具有潜在危险并且存在连接泄漏,当您自己检查连接时,您应该始终关闭它,可能会出现无法关闭连接的情况。

相反,我强烈建议使用 Spring 来管理您的事务和连接。

首先使用@Transactional(isolation=SERIALIZABLE) 注释您的方法。接下来将DataSourceTransactionManager&lt;tx:annotation-driven /&gt; 添加到您的配置中。在这些更改之后重写您拥有的数据访问代码。

JdbcTemplate jdbcTemplate = getJdbcTemplate(database);
final UserItems items;
if (!action.areItemsNew()) {
    items = jdbcTemplate.queryForObject("select * from tbl_items where ownerId = ? for update", userId, new UserItemsRowMapper());
} else {
    items = new UserItems();
}

action.doUserItemsAction(items);

String query = !action.areItemsNew() ? "update tbl_items set data=? where ownerId=?" : "insert into tbl_items(data,ownerId) values(?,?)";

byte[] data = SerializationUtils.serialize(items.getItemContainers());
jdbcTemplate.update(query, new SqlLobValue(data), userId);

类似的东西(连同上述修改应该可以工作)。 (这或多或少来自我的脑海,所以它可能需要一些调整)。使用适当的事务管理可确保所有内容都重用同一个连接而不是多个连接,还可以确保在完成或出现问题时将连接返回到池中。

我仍然建议使用不同的数据源,因为 C3P0 已经很老了。

【讨论】:

  • @你认为这段代码能保证同样的效果吗(在方法完成之前锁定行?谢谢!
  • 是的,由于事务分界和选择更新。
  • 谢谢。将立即尝试!我希望这能解决问题
  • 您可能不需要SERIALIZABLE,但可能默认就足够了(因为您使用select ... for update 锁定行)。
  • 这些可能源于您使用输出流创建byte[](或调用close方法)的事实。将其包装在 try/catch 块中,然后将 IOException 重新抛出为 RuntimeException
【解决方案2】:

所以,有几件事。

1) 您看到的“错误”消息不是错误,当 c3p0 记录消息以 DEBUG 开头的异常时,这意味着您正在记录在 DEBUG 级别,并且 c3p0 已生成异常只是为了捕获堆栈跟踪。 (c3p0 是一个旧库;Thread.getStackTrace() 在当时并不存在,创建异常是捕获和转储堆栈的一种方便方法。)您只是记录由于到期或测试失败而导致的池连接的预期破坏.一般来说,c3p0 期望在 INFO 上记录,在 DEBUG 级别会非常冗长。

2) 你没有死锁 c3p0 的线程池。如果是,您会看到APPARENT DEADLOCK 消息,然后是恢复。您遇到池耗尽的情况:客户端正在等待连接,但池位于 maxPoolSize 并且无法获取它们。

3) 池耗尽的常见原因是连接泄漏:在应用程序代码路径中的某处,在某些(可能是异常的)情况下,连接被获取,然后永远不会关闭()。您需要非常小心,以确保在 finally 块中可靠地关闭()连接,而不会由于 finally 块中的先前故障而无法跳过。在 Java 7+ 中,使用 try-with-resources。在旧版本中,使用reliable resource cleanup idiom

4) 要测试是否存在连接泄漏问题,请设置 c3p0 配置参数 unreturnedConnectionTimeoutdebugUnreturnedConnectionStackTracesunreturnedConnectionTimeout 可以解决这个问题,但是很糟糕。更重要的是,debugUnreturnedConnectionStackTraces 将向您显示问题所在,以便您可以修复它,并记录在 INFO 中打开未关闭异常的堆栈跟踪。 (您必须设置unreturnedConnectionTimeout 才能使debugUnreturnedConnectionStackTraces 生效;当连接超时被放弃时,会记录堆栈跟踪。)

5) 虽然 0.9.5-pre8 可能没问题,但 c3p0 的当前生产版本是 c3p0-0.9.5.1(取决于 mchange-commons-java v.0.2.10)。您可能会考虑使用它。我认为这与您的问题没有任何关系,但仍然。

我希望这会有所帮助!

更新:由于您现在发布的代码显示可能的连接泄漏,这里有一个关于如何修复它的建议。将 finally 块替换为:

} finally {
    if ( conn != null ) {
        try { if (!action.areItemsNew()) conn.commit(); }
        catch (SQLException e) {
           logger.error(e);
           throw new RuntimeException(e);
        } finally {
           conn.close()
        }
    }
}

更新 2: 上面的 redone finally 块将解决连接泄漏,但如果我是你,我也会更改此代码关于 commit() 的逻辑。这是一个建议的修订:

Connection conn = null;
ResultSet getItemsRS = null;

try {
    JdbcTemplate jdbcTemplate = getJdbcTemplate(database);

    conn = jdbcTemplate.getDataSource().getConnection();

    UserItems items;

    if (!action.areItemsNew()) {

        conn.setAutoCommit(false);
        conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

        PreparedStatement getItemsPS = conn.prepareStatement("select * from tbl_items where ownerId = ? for update",
                ResultSet.TYPE_FORWARD_ONLY,
                ResultSet.CONCUR_UPDATABLE);
        getItemsPS.setLong(1, userId);

        getItemsRS = getItemsPS.executeQuery();
        getItemsRS.next();

        items = new UserItemsRowMapper().mapRow(getItemsRS, getItemsRS.getRow());
    } else {
        items = new UserItems();
    }

    action.doUserItemsAction(items);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(items.getItemContainers());
    oos.close();
    byte[] data = baos.toByteArray();
    Blob blob = conn.createBlob();
    blob.setBytes(1, data);

    if (!action.areItemsNew()) {
        getItemsRS.updateBlob("data", blob);
        getItemsRS.updateRow();
        conn.commit();
    } else {
        jdbcTemplate.update("insert into tbl_items(ownerId,data) values(?,?)", userId, data);
    }
} catch (Exception e) {
    logger.error(e);
    throw new RuntimeException(e);
} finally {
    try { if ( conn != null ) conn.close(); }
    catch ( Exception e )
      { logger.error(e); }
}

现在commit() 将只被调用if (!action.areItemsNew()) 并且所有预期的操作都已成功。即使出现问题,commit() 也会被调用。资源清理代码也更加简单和干净。请注意,在建议的版本中,如果在 close() 上出现异常,则会记录它,但它不会被包装并作为 RuntimeException 重新抛出。通常,如果close() 上有一个异常,那么之前会有一个信息更丰富的异常,这就是你想看到的。如果发生异常的唯一地方是close(),则意味着所有数据库操作都已成功,因此您的应用程序可以在出现故障的情况下正确进行。 (如果close() 上有很多异常,最终您会耗尽连接池,但实际上只有当您的数据库或网络出现严重问题时才会发生这种情况。)

【讨论】:

  • 我已经设置了这两个参数 - unreturnedConnectionTimeout 和 debugUnreturnedConnectionStackTraces,正如我在问题中所写的那样。除了迫使卡住的连接释放之外,它没有给我任何见解。即使使用 debugUnreturnedConnectionStackTraces,c3p0 也没有记录任何错误,我已经为 com.mchange.v2.c3p0 包启用了调试日志记录。
  • 哦。所以这个问题在我写的时候你添加的代码中很明显:有两种方式连接可能无法关闭()ed:(1)`如果(!action.areItemsNew())`你只是忽略了关闭连接。 (2) 即使该条件为真,如果commit() 抛出异常,您对close() 的调用将被跳过。这段代码很可能会随着时间的推移和使用而泄漏Connections。
  • 就 c3p0 而言,只要不泄露连接,手动管理连接完全没有问题。有些人可能会发现将连接管理委托给中间件很方便,因为中间件会为他们处理这些事情并把事情做好(并不总是可靠的)。我自己的偏好是直接明确地管理连接。做起来并不难。
  • 我添加了对代码的建议修改,将 commit() 放在 try 块中,并大大简化了 finally 块。
  • @M.Deinum 是的,你是对的,我错过了,有一个调用将数据库的管理委托给 JdbcTemplate。正如您所说,从连接管理的角度来看,这不是问题,因为第二个连接将由 JdbcTemplate 在内部管理。人们对他们喜欢管理事物的水平的偏好不同,我们不必就此达成一致。如果您发现将事情委派给 JdbcTemplate 很有帮助,那就太好了。我们可能同意半途而废,直接管理一个连接但有条件地委托管理第二个连接有点尴尬。
猜你喜欢
  • 1970-01-01
  • 2016-08-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-23
  • 1970-01-01
  • 1970-01-01
  • 2016-06-16
相关资源
最近更新 更多