概述html
对于应用开发者来讲,数据链接泄漏无疑是一个可怕的梦魇。若是存在数据链接泄漏问题,应用程序将因数据链接资源的耗尽而崩溃,甚至还可能引发数据库的崩溃。数据链接泄漏像黑洞同样让开发者避之惟恐不及。java
Spring DAO 对全部支持的数据访问技术框架都使用模板化技术进行了薄层的封装。只要您的程序都使用 Spring DAO 模板(如 JdbcTemplate、HibernateTemplate 等)进行数据访问,必定不会存在数据链接泄漏的问题 ―― 这是 Spring 给予咱们郑重的承诺!所以,咱们无需关注数据链接(Connection)及其衍生品(Hibernate 的 Session 等)的获取和释放的操做,模板类已经经过其内部流程替咱们完成了,且对开发者是透明的。spring
可是因为集成第三方产品,整合遗产代码等缘由,可能须要直接访问数据源或直接获取数据链接及其衍生品。这时,若是使用不当,就可能在无心中创造出一个魔鬼般的链接泄漏问题。sql
咱们知道:当 Spring 事务方法运行时,就产生一个事务上下文,该上下文在本事务执行线程中针对同一个数据源绑定了一个惟一的数据链接(或其衍生品),全部被该事务上下文传播的 方法都共享这个数据链接。这个数据链接从数据源获取及返回给数据源都在 Spring 掌控之中,不会发生问题。若是在须要数据链接时,可以获取这个被 Spring 管控的数据链接,则使用者能够放心使用,无需关注链接释放的问题。数据库
那么,如何获取这些被 Spring 管控的数据链接呢? Spring 提供了两种方法:其一是使用数据资源获取工具类,其二是对数据源(或其衍生品如 Hibernate SessionFactory)进行代理。在具体介绍这些方法以前,让咱们先来看一下各类引起数据链接泄漏的场景。express
回页首apache
若是直接从数据源获取链接,且在使用完成后不主动归还给数据源(调用 Connection#close()),则将形成数据链接泄漏的问题。oracle
一个具体的实例app
下面,来看一个具体的实例:
package user.connleak; import org.apache.commons.dbcp.BasicDataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import java.sql.Connection; @Service("jdbcUserService") public class JdbcUserService { @Autowired private JdbcTemplate jdbcTemplate; public void logon(String userName) { try { // ①直接从数据源获取链接,后续程序没有显式释放该链接 Connection conn = jdbcTemplate.getDataSource().getConnection(); String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?"; jdbcTemplate.update(sql, System.currentTimeMillis(), userName); Thread.sleep(1000);// ②模拟程序代码的执行时间 } catch (Exception e) { e.printStackTrace(); } } } |
JdbcUserService 经过 Spring AOP 事务加强的配置,让全部 public 方法都工做在事务环境中。即让 logon() 和 updateLastLogonTime() 方法拥有事务功能。在 logon() 方法内部,咱们在①处经过调用 jdbcTemplate.getDataSource().getConnection()
显 式获取一个链接,这个链接不是 logon() 方法事务上下文线程绑定的链接,因此若是开发者若是没有手工释放这链接(显式调用 Connection#close() 方法),则这个链接将永久被占用(处于 active 状态),形成链接泄漏!下面,咱们编写模拟运行的代码,查看方法执行对数据链接的实际占用状况:
… @Service("jdbcUserService") public class JdbcUserService { … //①以异步线程的方式执行JdbcUserService#logon()方法,以模拟多线程的环境 public static void asynchrLogon(JdbcUserService userService, String userName) { UserServiceRunner runner = new UserServiceRunner(userService, userName); runner.start(); } private static class UserServiceRunner extends Thread { private JdbcUserService userService; private String userName; public UserServiceRunner(JdbcUserService userService, String userName) { this.userService = userService; this.userName = userName; } public void run() { userService.logon(userName); } } //② 让主执行线程睡眠一段指定的时间 public static void sleep(long time) { try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } //③ 汇报数据源的链接占用状况 public static void reportConn(BasicDataSource basicDataSource) { System.out.println("链接数[active:idle]-[" + basicDataSource.getNumActive()+":"+basicDataSource.getNumIdle()+"]"); } public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("user/connleak/applicatonContext.xml"); JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService"); BasicDataSource basicDataSource = (BasicDataSource) ctx.getBean("dataSource"); //④汇报数据源初始链接占用状况 JdbcUserService.reportConn(basicDataSource); JdbcUserService.asynchrLogon(userService, "tom"); JdbcUserService.sleep(500); //⑤此时线程A正在执行JdbcUserService#logon()方法 JdbcUserService.reportConn(basicDataSource); JdbcUserService.sleep(2000); //⑥此时线程A所执行的JdbcUserService#logon()方法已经执行完毕 JdbcUserService.reportConn(basicDataSource); JdbcUserService.asynchrLogon(userService, "john"); JdbcUserService.sleep(500); //⑦此时线程B正在执行JdbcUserService#logon()方法 JdbcUserService.reportConn(basicDataSource); JdbcUserService.sleep(2000); //⑧此时线程A和B都已完成JdbcUserService#logon()方法的执行 JdbcUserService.reportConn(basicDataSource); } |
在 JdbcUserService 中添加一个可异步执行 logon() 方法的 asynchrLogon() 方法,咱们经过异步执行 logon() 以及让主线程睡眠的方式模拟多线程环境下的执行场景。在不一样的执行点,经过 reportConn() 方法汇报数据源链接的占用状况。
使用以下的 Spring 配置文件对 JdbcUserServie 的方法进行事务加强:
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> <context:component-scan base-package="user.connleak"/> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="oracle.jdbc.driver.OracleDriver" p:url="jdbc:oracle:thin:@localhost:1521:orcl" p:username="test" p:password="test" p:defaultAutoCommit="false"/> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSource"/> <bean id="jdbcManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/> <!-- 对JdbcUserService的全部方法实施事务加强 --> <aop:config proxy-target-class="true"> <aop:pointcut id="serviceJdbcMethod" expression="within(user.connleak.JdbcUserService+)"/> <aop:advisor pointcut-ref="serviceJdbcMethod" advice-ref="jdbcAdvice" order="0"/> </aop:config> <tx:advice id="jdbcAdvice" transaction-manager="jdbcManager"> <tx:attributes> <tx:method name="*"/> </tx:attributes> </tx:advice> </beans> |
保证 BasicDataSource 数据源的配置默认链接为 0,运行以上程序代码,在控制台中将输出如下的信息:
链接数 [active:idle]-[0:0] 链接数 [active:idle]-[2:0] 链接数 [active:idle]-[1:1] 链接数 [active:idle]-[3:0] 链接数 [active:idle]-[2:1] |
咱们经过下表对数据源链接的占用和泄漏状况进行描述:
时间 | 执行线程 1 | 执行线程 2 | 数据源链接 | ||
---|---|---|---|---|---|
active | idle | leak | |||
T0 | 未启动 | 未启动 | 0 | 0 | 0 |
T1 | 正在执行方法 | 未启动 | 2 | 0 | 0 |
T2 | 执行完毕 | 未启动 | 1 | 1 | 1 |
T3 | 执行完毕 | 正式执行方法 | 3 | 0 | 1 |
T4 | 执行完毕 | 执行完毕 | 2 | 1 | 2 |
可见在执行线程 1 执行完毕后,只释放了一个数据链接,还有一个数据连处于 active 状态,说明泄漏了一个链接。类似的,执行线程 2 执行完毕后,也泄漏了一个链接:缘由是直接经过数据源获取链接 (jdbcTemplate.getDataSource().getConnection())而没有显式释放形成的。
Spring 提供了一个能从当前事务上下文中获取绑定的数据链接的工具类,那就是 DataSourceUtils。Spring 强调必须使用 DataSourceUtils 工具类获取数据链接,Spring 的 JdbcTemplate 内部也是经过 DataSourceUtils 来获取链接的。DataSourceUtils 提供了若干获取和释放数据链接的静态方法,说明以下:
static Connection doGetConnection(DataSource dataSource)
:首先尝试从事务上下文中获取链接,失败后再从数据源获取链接;static Connection getConnection(DataSource dataSource)
:和 doGetConnection 方法的功能同样,实际上,它内部就是调用 doGetConnection 方法获取链接的;static void doReleaseConnection(Connection con, DataSource dataSource)
:释放链接,放回到链接池中;static void releaseConnection(Connection con, DataSource dataSource)
:和 doReleaseConnection 方法的功能同样,实际上,它内部就是调用 doReleaseConnection 方法获取链接的;来看一下 DataSourceUtils 从数据源获取链接的关键代码:
public abstract class DataSourceUtils { … public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); //①首先尝试从事务同步管理器中获取数据链接 ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug( "Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(dataSource.getConnection()); } return conHolder.getConnection(); } //②若是获取不到,则直接从数据源中获取链接 Connection con = dataSource.getConnection(); //③若是拥有事务上下文,则将链接绑定到事务上下文中 if (TransactionSynchronizationManager.isSynchronizationActive()) { ConnectionHolder holderToUse = conHolder; if (holderToUse == null) { holderToUse = new ConnectionHolder(con); } else {holderToUse.setConnection(con);} holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization( new ConnectionSynchronization(holderToUse, dataSource)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { TransactionSynchronizationManager.bindResource( dataSource, holderToUse); } } return con; } … } |
它首先查看当前是否存在事务管理上下文,并尝试从事务管理上下文获取链接,若是获取失败,直接从数据源中获取链接。在获取链接后,若是当前拥有事务上下文,则将链接绑定到事务上下文中。
咱们在清单 1 的 JdbcUserService 中,使用 DataSourceUtils.getConnection() 替换直接从数据源中获取链接的代码:
public void logon(String userName) { try { //Connection conn = jdbcTemplate.getDataSource().getConnection(); //①使用DataSourceUtils获取数据链接 Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource()); String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?"; jdbcTemplate.update(sql, System.currentTimeMillis(), userName); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } |
从新运行代码,获得以下的执行结果:
链接数 [active:idle]-[0:0] 链接数 [active:idle]-[1:0] 链接数 [active:idle]-[0:1] 链接数 [active:idle]-[1:0] 链接数 [active:idle]-[0:1] |
对照清单 4 的输出日志,咱们能够看到已经没有链接泄漏的现象了。一个执行线程在运行 JdbcUserService#logon() 方法时,只占用一个链接,并且方法执行完毕后,该链接立刻释放。这说明经过 DataSourceUtils.getConnection() 方法确实获取了方法所在事务上下文绑定的那个链接,而不是像原来那样从数据源中获取一个新的链接。
使用 DataSourceUtils 获取数据链接也可能形成泄漏!
是否使用 DataSourceUtils 获取数据链接就能够高枕无忧了呢?理想很美好,但现实很残酷:若是 DataSourceUtils 在没有事务上下文的方法中使用 getConnection() 获取链接,依然会形成数据链接泄漏!
保持代码清单 6 的代码不变,调整 Spring 配置文件,将清单 3 中 Spring AOP 事务加强配置的代码注释掉,从新运行清单 6 的代码,将获得以下的输出日志:
链接数 [active:idle]-[0:0] 链接数 [active:idle]-[1:1] 链接数 [active:idle]-[1:1] 链接数 [active:idle]-[2:1] 链接数 [active:idle]-[2:1] |
咱们经过下表对数据源链接的占用和泄漏状况进行描述:
时间 | 执行线程 1 | 执行线程 2 | 数据源链接 | ||
---|---|---|---|---|---|
active | idle | leak | |||
T0 | 未启动 | 未启动 | 0 | 0 | 0 |
T1 | 正在执行方法 | 未启动 | 1 | 1 | 0 |
T2 | 执行完毕 | 未启动 | 1 | 1 | 1 |
T3 | 执行完毕 | 正式执行方法 | 2 | 1 | 1 |
T4 | 执行完毕 | 执行完毕 | 2 | 1 | 2 |
仔细对照表 1 的执行过程,咱们发如今 T1 时,有事务上下文时的 active 为 2,idle 为 0,而此时因为没有事务管理,则 active 为 1 而 idle 也为 1。这说明有事务上下文时,须要等到整个事务方法(即 logon())返回后,事务上下文绑定的链接才释放。但在没有事务上下文时,logon() 调用 JdbcTemplate 执行完数据操做后,立刻就释放链接。
在 T2 执行线程完成 logon() 方法的执行后,有一个链接没有被释放(active),因此发生了链接泄漏。到 T4 时,两个执行线程都完成了 logon() 方法的调用,可是出现了两个未释放的链接。
要堵上这个链接泄漏的漏洞,须要对 logon() 方法进行以下的改造:
public void logon(String userName) { Connection conn = null; try { conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource()); String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?"; jdbcTemplate.update(sql, System.currentTimeMillis(), userName); Thread.sleep(1000); // ① } catch (Exception e) { e.printStackTrace(); }finally { // ②显式使用DataSourceUtils释放链接 DataSourceUtils.releaseConnection(conn,jdbcTemplate.getDataSource()); } } |
在 ② 处显式调用 DataSourceUtils.releaseConnection()
方法释放获取的链接。特别须要指出的是:必定不能在 ① 处释放链接!由于若是 logon() 在获取链接后,① 处代码前这段代码执行时发生异常,则①处释放链接的动做将得不到执行。这将是一个很是具备隐蔽性的链接泄漏的隐患点。
分析 JdbcTemplate 的代码,咱们能够清楚地看到它开放的每一个数据操做方法,首先都使用 DataSourceUtils 获取链接,在方法返回以前使用 DataSourceUtils 释放链接。
来看一下 JdbcTemplate 最核心的一个数据操做方法 execute():
public <T> T execute(StatementCallback<T> action) throws DataAccessException { //① 首先根据DataSourceUtils获取数据链接 Connection con = DataSourceUtils.getConnection(getDataSource()); Statement stmt = null; try { Connection conToUse = con; … handleWarnings(stmt); return result; } catch (SQLException ex) { JdbcUtils.closeStatement(stmt); stmt = null; DataSourceUtils.releaseConnection(con, getDataSource()); con = null; throw getExceptionTranslator().translate( "StatementCallback", getSql(action), ex); } finally { JdbcUtils.closeStatement(stmt); //② 最后根据DataSourceUtils释放数据链接 DataSourceUtils.releaseConnection(con, getDataSource()); } } |
在 ① 处经过 DataSourceUtils.getConnection() 获取链接,在 ② 处经过 DataSourceUtils.releaseConnection() 释放链接。全部 JdbcTemplate 开放的数据访问方法最终都是经过 execute(StatementCallback<T> action)
执行数据访问操做的,所以这个方法表明了 JdbcTemplate 数据操做的最终实现方式。
正是由于 JdbcTemplate 严谨的获取链接,释放链接的模式化流程保证了 JdbcTemplate 对数据链接泄漏问题的免疫性。因此,若有可能尽可能使用 JdbcTemplate,HibernateTemplate 等这些模板进行数据访问操做,避免直接获取数据链接的操做。
使用 TransactionAwareDataSourceProxy
若是不得已要显式获取数据链接,除了使用 DataSourceUtils 获取事务上下文绑定的链接外,还能够经过 TransactionAwareDataSourceProxy 对数据源进行代理。数据源对象被代理后就具备了事务上下文感知的能力,经过代理数据源的 getConnection() 方法获取的链接和使用 DataSourceUtils.getConnection() 获取链接的效果是同样的。
下面是使用 TransactionAwareDataSourceProxy 对数据源进行代理的配置:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="oracle.jdbc.driver.OracleDriver" p:url="jdbc:oracle:thin:@localhost:1521:orcl" p:username="test" p:password="test" p:defaultAutoCommit="false"/> <!-- ①对数据源进行代理--> <bean id="dataSourceProxy" class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy" p:targetDataSource-ref="dataSource"/> <!-- ②直接使用数据源的代理对象--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSourceProxy"/> <!-- ③直接使用数据源的代理对象--> <bean id="jdbcManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSourceProxy"/> |
对数据源进行代理后,咱们就能够经过数据源代理对象的 getConnection() 获取事务上下文中绑定的数据链接了。
所以,若是数据源已经进行了 TransactionAwareDataSourceProxy 的代理,并且方法存在事务上下文,那么清单 1 的代码也不会生产链接泄漏的问题。
理解了 Spring JDBC 的数据链接泄漏问题,其中的道理能够平滑地推广到其它框架中去。Spring 为每一个数据访问技术框架都提供了一个获取事务上下文绑定的数据链接(或其衍生品)的工具类和数据源(或其衍生品)的代理类。
下表列出了不一样数据访问技术对应 DataSourceUtils 的等价类:
数据访问技术框架 | 链接 ( 或衍生品 ) 获取工具类 |
---|---|
Spring JDBC | org.springframework.jdbc.datasource.DataSourceUtils |
Hibernate | org.springframework.orm.hibernate3.SessionFactoryUtils |
iBatis | org.springframework.jdbc.datasource.DataSourceUtils |
JPA | org.springframework.orm.jpa.EntityManagerFactoryUtils |
JDO | org.springframework.orm.jdo.PersistenceManagerFactoryUtils |
TransactionAwareDataSourceProxy 的等价类
下表列出了不一样数据访问技术框架下 TransactionAwareDataSourceProxy 的等价类:
数据访问技术框架 | 链接 ( 或衍生品 ) 获取工具类 |
---|---|
Spring JDBC | org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy |
Hibernate | org.springframework.orm.hibernate3.LocalSessionFactoryBean |
iBatis | org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy |
JPA | 无 |
JDO | org.springframework.orm.jdo. TransactionAwarePersistenceManagerFactoryProxy |
在本文中,咱们经过剖析了解到如下的真相: