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()),則將形成數據鏈接泄漏的問題。 java

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狀態),形成鏈接泄漏!下面,咱們編寫模擬運行的代碼,查看方法執行對數據鏈接的實際佔用狀況:spring

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()方法進行事務加強,配置代碼以下所示: sql

<?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" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="${jdbc.driverClassName}" p:url="${jdbc.url}" p:username="${jdbc.username}" p:password="${jdbc.password}"/> <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>

而後,運行JdbcUserServie,在控制檯將觀察到以下的輸出信息:數據庫

引用
鏈接數[active:idle]-[0:0]
鏈接數[active:idle]-[2:0]
鏈接數[active:idle]-[1:1]
鏈接數[active:idle]-[3:0]
鏈接數[active:idle]-[2:1]

咱們經過表10-3對數據源鏈接的佔用和泄漏狀況進行描述。
時間 執行線程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())而沒有顯式釋 放。

經過DataSourceUtils獲取數據鏈接

    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):和doRelease Connection方法的功能同樣,實際上,它內部就是調用doReleaseConnection方法獲取鏈接的。


來看一下DataSourceUtils從數據源獲取鏈接的關鍵代碼: apache

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

從新運行代碼,獲得以下的執行結果:
app

引用
鏈接數[active:idle]-[0:0]
鏈接數[active:idle]-[1:0]
鏈接數[active:idle]-[0:1]
鏈接數[active:idle]-[1:0]
鏈接數[active:idle]-[0:1]


   對照上一節的輸出日誌,咱們能夠看到已經沒有鏈接泄漏的現象了。一個執行線程在運行JdbcUserService#logon()方法時,只佔用一個連 接,並且方法執行完畢後,該鏈接立刻釋放。這說明經過DataSourceUtils.getConnection()方法確實獲取了方法所在事務上下文 綁定的那個鏈接,而不是像原來那樣從數據源中獲取一個新的鏈接。

經過DataSourceUtils獲取數據鏈接

   是否使用DataSourceUtils獲取數據鏈接就能夠高枕無憂了呢?理想很美好,但現實很殘酷:若是DataSourceUtils在沒有事務上下文的方法中使用getConnection()獲取鏈接,依然會形成數據鏈接泄漏!
   保持上面的代碼不變,將上面Spring配置文件中①處的Spring AOP事務加強配置的代碼註釋掉,從新運行代碼清單10-23的代碼,將獲得以下的輸出日誌:

引用
鏈接數[active:idle]-[0:0]
鏈接數[active:idle]-[1:1]
鏈接數[active:idle]-[1:1]
鏈接數[active:idle]-[2:1]
鏈接數[active:idle]-[2:1]


    咱們經過下表對數據源鏈接的佔用和泄漏狀況進行描述。
    仔細對上表的執行過程,咱們發如今T1時,有事務上下文時的active爲2,idle爲0,而此時因爲沒有事務管理,則active爲1而idle也爲 1。這說明有事務上下文時,須要等到整個事務方法(即logon())返回後,事務上下文綁定的鏈接才被釋放。但在沒有事務上下文時,logon()調用 JdbcTemplate執行完數據操做後,立刻就釋放鏈接。

時間 執行線程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


   在T2執行線程完成logon()方法的調用後,有一個鏈接沒有被釋放(active),因此發生了鏈接泄漏。到T4時,兩個執行線程都完成了logon()方法的調用,可是出現了兩個未釋放的鏈接。

   要堵上這個鏈接泄漏的漏洞,須要對logon()方法進行以下的改造: 框架

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對數據源進行代理的配置: async

<?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" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="${jdbc.driverClassName}" p:url="${jdbc.url}" p:username="${jdbc.username}" p:password="${jdbc.password}"/> <!--②對數據源進行代碼,使數據源具體事務上下文感知性 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy" p:targetDataSource-ref="originDataSource" /> <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 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


  表10-6列出了不一樣數據訪問技術框架下TransactionAwareDataSourceProxy的等價類。
表10-6  不一樣數據訪問框架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


注:以上內容摘自《Spring 3.x企業應用開發實戰》
相關文章
相關標籤/搜索