防止保留未使用的数据库连接

Prevent keeping unused DB connection

问题描述:

让我们有一个从控制器调用的服务方法:

class PaymentService {
    static transactional = false

    public void pay(long id) {
        Member member = Member.get(id)
        //long running task executing HTTP request
        requestPayment(member)
    }
}

问题是如果8个用户同时点击同一个服务,执行requestPayment(member)方法的时间是30秒,整个应用会卡30秒。

问题比看起来更严重,因为如果 HTTP 请求执行良好,没有人会意识到任何问题。严重的问题是我们的 Web 服务的可用性取决于我们外部 partner/component(在我们的用例支付网关中)的可用性。因此,当您的合作伙伴开始出现性能问题时,您也会遇到这些问题,更糟糕的是,它会影响您应用的所有部分。

评价:

问题的原因是 Member.get(id) 从池中保留了一个数据库连接并保留它以供进一步使用,尽管 requestPayment(member) 方法永远不需要访问数据库。当下一个(第 9 个)请求到达需要数据库连接的应用程序的任何其他部分(事务服务,数据库 select,...)时,它会一直等待(如果 maxWait 设置为较低的持续时间,则超时)直到池有可用连接,在我们的用例中甚至可以持续 30 秒。

等待线程的堆栈跟踪是:

at java.lang.Object.wait(Object.java:-1)
      at java.lang.Object.wait(Object.java:485)
      at org.apache.commons.pool.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:1115)
      at org.apache.commons.dbcp.PoolingDataSource.getConnection(PoolingDataSource.java:106)
      at org.apache.commons.dbcp.BasicDataSource.getConnection(BasicDataSource.java:1044)
      at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:111)

或超时:

JDBC begin failed
org.apache.commons.dbcp.SQLNestedException: Cannot get a connection, pool error Timeout waiting for idle object
        at org.apache.commons.dbcp.PoolingDataSource.getConnection(PoolingDataSource.java:114)
        at org.apache.commons.dbcp.BasicDataSource.getConnection(BasicDataSource.java:1044)
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object
        at org.apache.commons.pool.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:1167)
        at org.apache.commons.dbcp.PoolingDataSource.getConnection(PoolingDataSource.java:106)
        ... 7 more

显然,同样的问题也发生在交易服务上,但它更有意义,因为连接是为交易保留的。

作为一个临时解决方案,可以在数据源上使用 maxActive 属性 来增加池大小,但是它并没有解决保持未使用连接的实际问题.

作为永久解决方案,可以将所有数据库操作包含在事务行为中(withTransaction{..}@Transactional),returns提交后连接回池(或者令我惊讶的是 withNewSession{..} 也有效)。但是我们需要确保从控制器到 requestPayment(member) 方法的整个调用链不会泄漏连接。

如果连接是 "leaked"(类似于 Propagation.NEVER 事务行为),我希望能够在 requestPayment(member) 方法中抛出异常,这样我就可以揭示在测试阶段早期发布。

在深入研究源代码后,我找到了解决方案:

class PaymentService {
    static transactional = false
    def sessionFactory

    public void pay(long id) {
        Member member = Member.get(id)
        sessionFactory.currentSession.disconnect()
        //long running task executing HTTP request
        requestPayment(member)
    }
}

以上语句将连接释放回池中。

如果从事务上下文执行,则会抛出异常 (org.hibernate.HibernateException connnection proxy not usable after transaction completion),因为我们无法释放这样的连接(这正是我所需要的)。

Javadoc:

Disconnect the Session from the current JDBC connection. If the connection was obtained by Hibernate close it and return it to the connection pool; otherwise, return it to the application.

This is used by applications which supply JDBC connections to Hibernate and which require long-sessions (or long-conversations)