问题描述:
让我们有一个从 Controller 调用的服务方法:
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 请求执行良好,没有人会意识到有什么问题。严重的问题是我们的网络服务的可用性取决于我们的外部合作伙伴/组件(在我们的用例支付网关中)的可用性。因此,当您的合作伙伴开始出现性能问题时,您也会遇到这些问题,更糟糕的是,它会影响您应用程序的所有部分。
评价:
问题的原因是 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
显然,事务服务也会发生同样的问题,但它更有意义,因为连接是为事务保留的。
作为临时解决方案,可以使用数据源上的 maxActive 属性增加池大小,但它并不能解决保持未使用连接的真正问题。
作为永久解决方案,可以将所有数据库操作包含在事务行为中(withTransaction{..}
、@Transactional
),这提交后将连接返回到池(或者令我惊讶的是 withNewSession{..}
也有效)。但我们需要确保从 Controller 到 requestPayment(member) 方法的整个调用链不会泄漏连接。
如果连接“泄漏”,我希望能够在 requestPayment(member)
方法中引发异常(类似于 Propagation.NEVER
事务行为) ),这样我就可以在测试阶段尽早揭示问题。
最佳答案
深入研究源代码后,我找到了解决方案:
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 连接代理在事务完成后不可用
),因为我们无法释放这样的连接(这正是我想要的)需要)。
Java 文档:
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)
关于grails - 防止保留未使用的数据库连接,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35364394/