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

时间:2016-02-12 13:57:46

标签: grails transactions connection-pooling

问题描述:

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

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服务的可用性取决于我们的外部合作伙伴/组件(在我们的用例支付网关中)的可用性。因此,当您的合作伙伴开始遇到性能问题时,您也会遇到性能问题,更糟糕的是它会影响您应用的所有部分。

评价为:

问题的原因是Member.get(id)保留了来自池的数据库连接,并保留了它以供进一步使用,尽管requestPayment(member)方法永远不需要访问数据库。当下一个(第9个)请求命中应用程序的任何其他需要数据库连接的部分(事务服务,数据库选择,...)时,它会一直等待(或者如果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

显然,事务性服务会出现同样的问题,但由于为事务保留了连接,因此更有意义。

作为临时解决方案,可以使用datasource上的maxActive属性来增加池大小,但是它无法解决保持未使用连接的真正问题。

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

如果连接“泄露”(类似于requestPayment(member)事务行为),我希望能够在Propagation.NEVER方法中抛出异常,因此我可以在测试期间尽早揭示问题相。

1 个答案:

答案 0 :(得分:2)

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

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:

  

断开Session与当前JDBC连接的连接。如果   连接是通过Hibernate获得关闭它并将其返回到   连接池;否则,将其退回申请表。

     

这是由提供JDBC连接的应用程序使用的   Hibernate,需要长会话(或长会话)