程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Spring事務管理高級應用難點剖析: 第3部分

Spring事務管理高級應用難點剖析: 第3部分

編輯:關於JAVA

概述

對於應用開發者來說,數據連接洩漏無疑是一個可怕的夢魇。如果存在數據連 接洩漏問題,應用程序將因數據連接資源的耗盡而崩潰,甚至還可能引起數據庫的崩潰。數據 連接洩漏像黑洞一樣讓開發者避之唯恐不及。

Spring DAO 對所有支持的數據訪問技術 框架都使用模板化技術進行了薄層的封裝。只要您的程序都使用 Spring DAO 模板(如 JdbcTemplate、HibernateTemplate 等)進行數據訪問,一定不會存在數據連接洩漏的問題 ― ― 這是 Spring 給予我們鄭重的承諾!因此,我們無需關注數據連接(Connection)及其衍生 品(Hibernate 的 Session 等)的獲取和釋放的操作,模板類已經通過其內部流程替我們完成 了,且對開發者是透明的。

但是由於集成第三方產品,整合遺產代碼等原因,可能需要 直接訪問數據源或直接獲取數據連接及其衍生品。這時,如果使用不當,就可能在無意中創造 出一個魔鬼般的連接洩漏問題。

我們知道:當 Spring 事務方法運行時,就產生一個事 務上下文,該上下文在本事務執行線程中針對同一個數據源綁定了一個唯一的數據連接(或其 衍生品),所有被該事務上下文傳播的方法都共享這個數據連接。這個數據連接從數據源獲取 及返回給數據源都在 Spring 掌控之中,不會發生問題。如果在需要數據連接時,能夠獲取這 個被 Spring 管控的數據連接,則使用者可以放心使用,無需關注連接釋放的問題。

那 麼,如何獲取這些被 Spring 管控的數據連接呢? Spring 提供了兩種方法:其一是使用數據 資源獲取工具類,其二是對數據源(或其衍生品如 Hibernate SessionFactory)進行代理。在 具體介紹這些方法之前,讓我們先來看一下各種引發數據連接洩漏的場景。

Spring JDBC 數據連接洩漏

如果直接從數據源獲取連接,且在使用完成後不主動歸還給數據源 (調用 Connection#close()),則將造成數據連接洩漏的問題。

一個具體的實例

下面,來看一個具體的實例:

清單 1.JdbcUserService.java:主體代碼

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 狀態),造成連接洩漏!下面,我們編寫模擬運行的代碼,查看方法執行對數據連接的 實際占用情況:

清單 2.JdbcUserService.java:模擬運行代碼


@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 的方法進行事務增強:

清單 3.applicationContext.xml

<?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,運行以 上程序代碼,在控制台中將輸出以下的信息:

清單 4. 輸出日志

連接數  [active:idle]-[0:0]
連接數 [active:idle]-[2:0]
連接數 [active:idle]- [1:1]
連接數 [active:idle]-[3:0]
連接數 [active:idle]-[2:1]

我們通過下表對數據源連接的占用和洩漏情況進行描述:

表 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())而沒有顯式釋放造成的。

通過 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):和 doReleaseConnection 方法的功能一樣,實際上,它內部就是調用 doReleaseConnection 方法 獲取連接的;

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

清單 5. DataSourceUtils.java 獲取連接的工具類

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() 替換直接從數據源中獲取連接 的代碼:

清單 6. JdbcUserService.java:使用 DataSourceUtils 獲取數據連接

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

重新運行代碼,得到如下的執行結 果:

清單 7. 輸出日志

連接數 [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 的代碼,將得到如下的輸出日志:

清單 8. 輸出日志

連接數  [active:idle]-[0:0]
連接數 [active:idle]-[1:1]
連接數 [active:idle]- [1:1]
連接數 [active:idle]-[2:1]
連接數 [active:idle]-[2:1]

我們通過下表對數據源連接的占用和洩漏情況進行描述:

表 2. 執行過 程數據源連接占用情況

時間 執行線程 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() 方法進行如下的改造:

清單 9.JdbcUserService.java:手工釋放獲取的連接

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 如何做到對連接洩漏的免疫

分析 JdbcTemplate 的代碼,我們可 以清楚地看到它開放的每個數據操作方法,首先都使用 DataSourceUtils 獲取連接,在方法返 回之前使用 DataSourceUtils 釋放連接。

來看一下 JdbcTemplate 最核心的一個數據 操作方法 execute():

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

清單 11.applicationContext.xml:對數據源進行代理

<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 的等價類

下表列出了不同數據訪問技術對應 DataSourceUtils 的等價類:

表 3. 不同數據訪問 框架 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 的等價類:

表 4. 不同 數據訪問框架 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 JDBC 時如果直接獲 取 Connection,可能會造成連接洩漏。為降低連接洩漏的可能,盡量使用 DataSourceUtils 獲取數據連接。也可以對數據源進行代理,以便將其擁有事務上下文的感知能力;

可以 將 Spring JDBC 防止連接洩漏的解決方案平滑應用到其它的數據訪問技術框架中。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved