底層連接資源的訪問問題 對于應用開發者來說,數據連接泄漏無疑是一個可怕的夢魘。只要你開發的應用存在數據連接泄漏的問題,應用程序最終都將因數據連接資源的耗盡而崩潰,甚至還可能引起數據庫的崩潰。數據連接泄漏像一個黑洞那樣讓開發者避之唯恐不及。
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" 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從數據源獲取連接的關鍵代碼:
- 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();
- }
- }
- }
重新運行代碼,得到如下的執行結果:
引用
連接數[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對數據源進行代理的配置:
- <?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企業應用開發實戰》