读书人

Spring的事务管理难题剖析(7):数据连接

发布时间: 2012-09-18 16:21:42 作者: rapoo

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();        }    }}

JdbcUserService通过Spring AOP事务增强的配置,让所有public方法都工作在事务环境中,即让logon()和updateLastLogonTime()方法拥有事务功能。在logon()方法内部,我们在①处通过调用jdbcTemplate.getDataSource().getConnection()显式获取一个连接,这个连接不是logon()方法事务上下文线程绑定的连接,所以如果开发者没有手工释放这个连接(显式调用Connection#close()方法),则这个连接将永久被占用(处于active状态),造成连接泄漏!下面,我们编写模拟运行的代码,查看方法执行对数据连接的实际占用情况:
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);    }


在JdbcUserService中添加一个可异步执行logon()方法的asynchrLogon()方法,我们通过异步执行logon()以及让主线程睡眠的方式模拟多线程环境下的执行场景。在不同的执行点,通过reportConn()方法汇报数据源连接的占用情况。
通过Spring事务声明,对JdbcUserServie的logon()方法进行事务增强,配置代码如下所示:
<?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;}     …}


它首先查看当前是否存在事务管理上下文,并尝试从事务管理上下文获取连接,如果获取失败,直接从数据源中获取连接。在获取连接后,如果当前拥有事务上下文,则将连接绑定到事务上下文中。
我们在JdbcUserService中,使用DataSourceUtils.getConnection()替换直接从数据源中获取连接的代码:
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();        }    }}


重新运行代码,得到如下的执行结果:

package com.baobaotao.connleak;…@Service("jdbcUserService")public class JdbcUserService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional public void logon(String userName) { try { 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(); }finally { //②显式使用DataSourceUtils释放连接 DataSourceUtils.releaseConnection(conn,jdbcTemplate.getDataSource()); } }}
在②处显式调用DataSourceUtils.releaseConnection()方法释放获取的连接。特别需要指出的是:一定不能在①处释放连接!因为如果logon()在获取连接后,①处代码前这段代码执行时发生异常,则①处释放连接的动作将得不到执行。这将是一个非常具有隐蔽性的连接泄漏的隐患点。

JdbcTemplate如何做到对连接泄漏的免疫

分析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释放数据连接              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开放的数据访问API最终都是直接或间接由execute(StatementCallback<T> action)方法执行数据访问操作的,因此这个方法代表了JdbcTemplate数据操作的最终实现方式。
正是因为JdbcTemplate严谨的获取连接及释放连接的模式化流程保证了JdbcTemplate对数据连接泄漏问题的免疫性。所以,如有可能尽量使用JdbcTemplate、HibernateTemplate等这些模板进行数据访问操作,避免直接获取数据连接的操作。

使用TransactionAwareDataSourceProxy


如果不得已要显式获取数据连接,除了使用DataSourceUtils获取事务上下文绑定的连接外,还可以通过TransactionAwareDataSourceProxy对数据源进行代理。数据源对象被代理后就具有了事务上下文感知的能力,通过代理数据源的getConnection()方法获取连接和使用DataSourceUtils.getConnection()获取连接的效果是一样的。
下面是使用TransactionAwareDataSourceProxy对数据源进行代理的配置:

<?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>


对数据源进行代理后,我们就可以通过数据源代理对象的getConnection()获取事务上下文中绑定的数据连接了。因此,如果数据源已经进行了 TransactionAwareDataSourceProxy的代理,而且方法存在事务上下文,那么代码清单10-19的代码也不会生产连接泄漏的问题。

其他数据访问技术的等价类

理解了Spring JDBC的数据连接泄漏问题,其中的道理可以平滑地推广到其他框架中去。Spring为每个数据访问技术框架都提供了一个获取事务上下文绑定的数据连接(或其衍生品)的工具类和数据源(或其衍生品)的代理类。
表10-5列出了不同数据访问技术对应DataSourceUtils的等价类。
表10-5 不同数据访问框架DataSourceUtils的等价类
数据访问技术框架连接(或衍生品)获取工具类 Spring JDBCorg.springframework.jdbc.datasource.DataSourceUtils Hibernateorg.springframework.orm.hibernate3.SessionFactoryUtils iBatisorg.springframework.jdbc.datasource.DataSourceUtils JPAorg.springframework.orm.jpa.EntityManagerFactoryUtils JDOorg.springframework.orm.jdo.PersistenceManagerFactoryUtils

表10-6列出了不同数据访问技术框架下TransactionAwareDataSourceProxy的等价类。
表10-6 不同数据访问框架TransactionAwareDataSourceProxy的等价类
数据访问技术框架连接(或衍生品)获取工具类 Spring JDBCorg.springframework.jdbc.datasource.TransactionAwareDataSourceProxy Hibernateorg.springframework.orm.hibernate3.LocalSessionFactoryBean iBatisorg.springframework.jdbc.datasource.TransactionAwareDataSourceProxy JPA无 JDOorg.springframework.orm.jdo.TransactionAwarePersistenceManagerFactoryProxy

注:以上内容摘自《Spring 3.x企业应用开发实战》 1 楼 jinnianshilongnian 2012-03-09 1、打开连接不关闭 这是程序员犯的不该犯的错误(发现错误后可以改)

2、当有事务方法 特别慢,会拖慢整个应用 甚至造成死锁。
以前我们用c3p0曾经遇到过类似的,在用户注册高峰时,由于赠送积分/还有一些道具之类的是和另一个系统集成的,所以在高峰期特别慢,从而导致整个注册方法特别慢,最后改成异步赠送(失败了影响也是比较小的)。

因此第一种情况,是可以查找并改正的。
第二种情况,需要实际情况实际分析。 2 楼 huang_yong 2012-04-14 写程序的时候,如果是从底层获取Connection,一定要注意在finally里面close掉,但是如果从Hibernate的SessionFactory级别获取Connection,就无需手工close掉,这些工作都由Hibernate为我们完成了。

一定要使用:

Connection conn = SessionFactoryUtils.getDataSource(hibernateTemplate.getSessionFactory()).getConnection();

获取Connection,以上代码可以进行封装,放入公共类中,方便其他程序员使用。

读书人网 >编程

热点推荐