Spring的事务管理难点剖析(7):数据连接泄漏
底层连接资源的访问问题
对于应用开发者来说,数据连接泄漏无疑是一个可怕的梦魇。只要你开发的应用存在数据连接泄漏的问题,应用程序最终都将因数据连接资源的耗尽而崩溃,甚至还可能引起数据库的崩溃。数据连接泄漏像一个黑洞那样让开发者避之唯恐不及。
Spring DAO对所有支持的数据访问技术框架都使用模板化技术进行了薄层的封装。只要你的程序都使用Spring DAO的模板(如JdbcTemplate、HibernateTemplate等)进行数据访问,一定不会存在数据连接泄漏的问题——这是Spring给予我们的郑重承诺!如果使用Spring DAO模板进行数据操作,我们无须关注数据连接(Connection)及其衍生品(Hibernate的Session等)的获取和释放操作,模板类已经通过其内部流程替我们完成了,且对开发者是透明的。
但是由于集成第三方产品、整合遗产代码等原因,可能需要直接访问数据源或直接获取数据连接及其衍生品。这时,如果使用不当,就可能在无意中创造出一个魔鬼般的连接泄漏问题。
我们知道:当Spring事务方法运行时,就产生一个事务上下文,该上下文在本事务执行线程中针对同一个数据源绑定了一个唯一的数据连接(或其衍生品),所有被该事务上下文传播的方法都共享这个数据连接。这个数据连接从数据源获取及返回给数据源都在Spring掌控之中,不会发生问题。如果在需要数据连接时,能够获取这个被Spring管控的数据连接,则使用者可以放心使用,无须关注连接释放的问题。
那么,如何获取这些被Spring管控的数据连接呢?Spring提供了两种方法:其一是使用数据资源获取工具类;其二是对数据源(或其衍生品如Hibernate SessionFactory)进行代理。
Spring JDBC数据连接泄漏
如果我们从数据源直接获取连接,且在使用完成后不主动归还给数据源(调用Connection#close()),则将造成数据连接泄漏的问题。
package com.baobaotao.connleak;…@Service("jdbcUserService")public class JdbcUserService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional 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(); } }}package com.baobaotao.connleak;…@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("com/baobaotao/connleak/applicatonContext.xml"); JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService"); BasicDataSource basicDataSource = (BasicDataSource) ctx.getBean("dataSource"); //④汇报数据源初始连接占用情况 JdbcUserService.reportConn(basicDataSource); JdbcUserService.asynchrLogon(userService, "tom");//启动一个异常线程A JdbcUserService.sleep(500); //⑤此时线程A正在执行JdbcUserService#logon()方法 JdbcUserService.reportConn(basicDataSource); JdbcUserService.sleep(2000); //⑥此时线程A所执行的JdbcUserService#logon()方法已经执行完毕 JdbcUserService.reportConn(basicDataSource); JdbcUserService.asynchrLogon(userService, "john");//启动一个异常线程B JdbcUserService.sleep(500); //⑦此时线程B正在执行JdbcUserService#logon()方法 JdbcUserService.reportConn(basicDataSource); JdbcUserService.sleep(2000); //⑧此时线程A和B都已完成JdbcUserService#logon()方法的执行 JdbcUserService.reportConn(basicDataSource); }<?xml version="1.0" encoding="UTF-8" ?><beans xmlns="http://www.springframework.org/schema/beans" … http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> <context:component-scan base-package="com.baobaotao.connleak"/> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" p:driverClassName="${jdbc.driverClassName}"p:url="${jdbc.url}" p:username="${jdbc.username}"p:password="${jdbc.password}"/> <bean id="jdbcTemplate" name="code">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;} …}package com.baobaotao.connleak;…@Service("jdbcUserService")public class JdbcUserService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional public void logon(String userName) { try { //①使用DataSourceUtils获取数据连接 Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource()); //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(); } }}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释放数据连接 DataSourceUtils.releaseConnection(con, getDataSource());con = null;throw getExceptionTranslator().translate( "StatementCallback", getSql(action), ex);}finally {JdbcUtils.closeStatement(stmt); //③最后再使用DataSourceUtils释放数据连接DataSourceUtils.releaseConnection(con, getDataSource());}}<?xml version="1.0" encoding="UTF-8" ?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" … http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> <context:component-scan base-package="com.baobaotao.connleak"/> <context:property-placeholder location="classpath:jdbc.properties"/> <!--①未被代理的数据源 --> <bean id="originDataSource" p:driverClassName="${jdbc.driverClassName}"p:url="${jdbc.url}" p:username="${jdbc.username}"p:password="${jdbc.password}"/> <!--②对数据源进行代码,使数据源具体事务上下文感知性 --><bean id="dataSource"/> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSource"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/> <tx:annotation-driven/> </beans>