防止保留未使用的数据库连接
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)
问题描述:
让我们有一个从控制器调用的服务方法:
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)