概述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 |
在本文中,咱們經過剖析瞭解到如下的真相: