javascript
Spring JDBC-数据连接泄露解读
- 概述
- 示例數據連接泄露演示
- 事務環境下通過DataSourceUtils獲取數據連接
- 非事務環境下通過DataSourceUtils獲取數據連接也可能造成泄漏
- JdbcTemplate 如何做到對連接泄漏的免疫
- 使用 TransactionAwareDataSourceProxy
- 其它數據訪問技術的等價類
- 總結
- 示例源碼
概述
數據連接泄漏無疑是一個可怕的夢魘。如果存在數據連接泄漏問題,應用程序將因數據連接資源的耗盡而崩潰,甚至還可能引起數據庫的崩潰。
Spring DAO 對所有支持的數據訪問技術框架都使用模板化技術進行了薄層的封裝。只要我們的應用程序都使用 Spring DAO 模板(如 JdbcTemplate、HibernateTemplate 等)進行數據訪問,一定不會存在數據連接泄漏的問題 。
因此,我們無需關注數據連接(Connection)及其衍生品(Hibernate 的 Session 等)的獲取和釋放的操作,模板類已經通過其內部流程替我們完成了,且對開發者是透明的。
但是由于集成第三方產品,整合遺產代碼等原因,可能需要直接訪問數據源或直接獲取數據連接及其衍生品。這時,如果使用不當,就可能在無意中創造出一個魔鬼般的連接泄漏問題。
眾所周知,當 Spring 事務方法運行時,就產生一個事務上下文,該上下文在本事務執行線程中針對同一個數據源綁定了一個唯一的數據連接(或其衍生品),所有被該事務上下文傳播的方法都共享這個數據連接。這個數據連接從數據源獲取及返回給數據源都在 Spring 掌控之中,不會發生問題。如果在需要數據連接時,能夠獲取這個被 Spring 管控的數據連接,則我們可以放心使用,無需關注連接釋放的問題。
那如何獲取這些被 Spring 管控的數據連接呢? Spring 提供了兩種方法:
其一是使用數據資源獲取工具類
其二是對數據源(或其衍生品如 Hibernate SessionFactory)進行代理。
示例:數據連接泄露演示
在具體介紹這些方法之前,讓我們先來看一下各種引發數據連接泄漏的場景。
package com.xgj.dao.transaction.dbConnleak;import java.sql.Connection; import java.sql.SQLException;import org.apache.commons.dbcp.BasicDataSource; import org.apache.log4j.Logger; 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;@Service public class JdbcStudentService {private Logger logger = Logger.getLogger(JdbcStudentService.class);private static final String addStudentSQL = "insert into student(id,name,age,sex) values(student_id_seq.nextval,?,?,?)";private JdbcTemplate jdbcTemplate;@Autowiredpublic void setJdbcTemplate(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}public void addStudent(Student student) {try {// (0)直接從數據源獲取連接,后續程序沒有顯式釋放該連接Connection connection = jdbcTemplate.getDataSource().getConnection();jdbcTemplate.update(addStudentSQL, student.getName(),student.getAge(), student.getSex());Thread.sleep(1000);// (0-1)模擬程序代碼的執行時間logger.info("addStudent successfully");} catch (SQLException | InterruptedException e) {e.printStackTrace();}}}JdbcStudentService通過 Spring AOP 事務增強的配置,讓所有 public 方法都工作在事務環境中。即讓addStudent()方法擁有事務功能。在 addStudent() 方法內部,我們在(0)處通過調用 jdbcTemplate.getDataSource().getConnection()顯式獲取一個連接,這個連接不是 addStudent() 方法事務上下文線程綁定的連接,所以如果我們如果沒有手工釋放這連接(顯式調用 Connection#close() 方法),則這個連接將永久被占用(處于 active 狀態),造成連接泄漏!
下面,我們編寫模擬運行的代碼,查看方法執行對數據連接的實際占用情況
// (1)以異步線程的方式執行JdbcStudentService#addStudent()方法,以模擬多線程的環境public static void asynchrLogon(JdbcStudentService userService,Student student) {StudentServiceRunner runner = new StudentServiceRunner(userService,student);runner.start();}private static class StudentServiceRunner extends Thread {private JdbcStudentService studentService;private Student student;public StudentServiceRunner(JdbcStudentService studentService,Student student) {this.studentService = studentService;this.student = student;}public void run() {studentService.addStudent(student);}}// (2) 讓主執行線程睡眠一段指定的時間public static void sleep(long time) {try {Thread.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}}/*** * * @Title: reportConn* * @Description: (3)匯報數據源的連接占用情況* * @param basicDataSource* * @return: void*/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/xgj/dao/transaction/dbConnleak/conf_conn_leak.xml");JdbcStudentService jdbcStudentService = (JdbcStudentService) ctx.getBean("jdbcStudentService");BasicDataSource basicDataSource = (BasicDataSource) ctx.getBean("dataSource");// (4)匯報數據源初始連接占用情況JdbcStudentService.reportConn(basicDataSource);Student student = new Student();student.setAge(20);student.setName("LEAK");student.setSex("MALE");JdbcStudentService.asynchrLogon(jdbcStudentService, student);JdbcStudentService.sleep(500);// (5)此時線程A正在執行JdbcStudentService#addStudent()方法JdbcStudentService.reportConn(basicDataSource);JdbcStudentService.sleep(2000);// (6)此時線程A所執行的JdbcStudentService#addStudent()方法已經執行完畢JdbcStudentService.reportConn(basicDataSource);JdbcStudentService.asynchrLogon(jdbcStudentService, student);JdbcStudentService.sleep(500);// (7)此時線程B正在執行JdbcStudentService#addStudent()方法JdbcStudentService.reportConn(basicDataSource);JdbcStudentService.sleep(2000);// (8)此時線程A和B都已完成JdbcStudentService#addStudent()方法的執行JdbcStudentService.reportConn(basicDataSource);}在 JdbcStudentService中添加一個可異步執行 addStudent() 方法的 asynchrLogon() 方法,我們通過異步執行 addStudent() 以及讓主線程睡眠的方式模擬多線程環境下的執行場景。在不同的執行點,通過 reportConn() 方法匯報數據源連接的占用情況。
配置文件
<?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:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"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.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsdhttp://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx.xsd"><!-- 掃描類包,將標注Spring注解的類自動轉化Bean,同時完成Bean的注入 --><context:component-scan base-package="com.xgj.dao.transaction.dbConnleak" /><!-- 使用context命名空間,配置數據庫的properties文件 --><context:property-placeholder location="classpath:spring/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}" /><!-- 配置Jdbc模板 --><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"/><!-- 通過aop 配置事務增強 --><aop:config proxy-target-class="true"><!-- 切點 --><aop:pointcut id="serviceJdbcMethod" expression="within(com.xgj.dao.transaction.dbConnleak.JdbcStudentService)"/><!-- 切面 --><aop:advisor pointcut-ref="serviceJdbcMethod" advice-ref="txAdvice"/></aop:config><!-- 增強,供aop:advisor引用 --><tx:advice id="txAdvice" transaction-manager="jdbcManager"><tx:attributes><tx:method name="*"/></tx:attributes></tx:advice></beans>保證 BasicDataSource 數據源的配置默認連接為 0,運行程序
2017-09-26 22:38:26,862 INFO [main] (AbstractApplicationContext.java:583) - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@4680937b: startup date [Tue Sep 26 22:38:26 BOT 2017]; root of context hierarchy 2017-09-26 22:38:26,951 INFO [main] (XmlBeanDefinitionReader.java:317) - Loading XML bean definitions from class path resource [com/xgj/dao/transaction/dbConnleak/conf_conn_leak.xml] 連接數[active:idle]-[0:0] 連接數[active:idle]-[1:0] 2017-09-26 22:38:29,975 INFO [Thread-1] (JdbcStudentService.java:35) - addStudent successfully 連接數[active:idle]-[1:1] 連接數[active:idle]-[3:0] 2017-09-26 22:38:31,872 INFO [Thread-2] (JdbcStudentService.java:35) - addStudent successfully 連接數[active:idle]-[2:1]我們通過下表對數據源連接的占用和泄漏情況進行描述
可見在執行線程 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, DataSourcedataSource):釋放連接,放回到連接池中;
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;}… }它首先查看當前是否存在事務管理上下文,并嘗試從事務管理上下文獲取連接,如果獲取失敗,直接從數據源中獲取連接。在獲取連接后,如果當前擁有事務上下文,則將連接綁定到事務上下文中。
我們對上面那個有連接泄露的方法進行改造,使用 DataSourceUtils.getConnection() 替換直接從數據源中獲取連接的代碼:
public void addStudent(Student student) {try {// (0)直接從數據源獲取連接,后續程序沒有顯式釋放該連接// Connection connection = jdbcTemplate.getDataSource()// .getConnection();// 在事務環境下,通過DataSourceUtils獲取數據連接Connection coon = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());jdbcTemplate.update(addStudentSQL, student.getName(),student.getAge(), student.getSex());Thread.sleep(1000);// (0-1)模擬程序代碼的執行時間logger.info("addStudent successfully");} catch (Exception e) {e.printStackTrace();}}重新運行日志如下:
2017-09-26 23:19:32,588 INFO [main] (AbstractApplicationContext.java:583) - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@2c686c5e: startup date [Tue Sep 26 23:19:32 BOT 2017]; root of context hierarchy 2017-09-26 23:19:32,719 INFO [main] (XmlBeanDefinitionReader.java:317) - Loading XML bean definitions from class path resource [com/xgj/dao/transaction/dbConnleak/conf_conn_leak.xml] 連接數[active:idle]-[0:0] 連接數[active:idle]-[0:0] 2017-09-26 23:19:36,716 INFO [Thread-1] (JdbcStudentService.java:40) - addStudent successfully 連接數[active:idle]-[0:1] 連接數[active:idle]-[1:0] 2017-09-26 23:19:38,273 INFO [Thread-2] (JdbcStudentService.java:40) - addStudent successfully 連接數[active:idle]-[0:1]我們可以看到已經沒有連接泄漏的現象了。一個執行線程在運行 JdbcStudentService#addStudent() 方法時,只占用一個連接,而且方法執行完畢后,該連接馬上釋放。這說明通過 DataSourceUtils.getConnection() 方法確實獲取了方法所在事務上下文綁定的那個連接,而不是像原來那樣從數據源中獲取一個新的連接。
非事務環境下通過DataSourceUtils獲取數據連接也可能造成泄漏
如果 DataSourceUtils 在沒有事務上下文的方法中使用 getConnection() 獲取連接,依然會造成數據連接泄漏!
我們保持使用DataSourceUtils獲取數據源的代碼不變,修改下配置文件中的AOP增強,去掉事務增強(如下部分)
<!-- 通過aop 配置事務增強 --><aop:config proxy-target-class="true"><!-- 切點 --><aop:pointcut id="serviceJdbcMethod" expression="within(com.xgj.dao.transaction.dbConnleak.JdbcStudentService)"/><!-- 切面 --><aop:advisor pointcut-ref="serviceJdbcMethod" advice-ref="txAdvice"/></aop:config><!-- 增強,供aop:advisor引用 --><tx:advice id="txAdvice" transaction-manager="jdbcManager"><tx:attributes><tx:method name="*"/></tx:attributes></tx:advice>再此運行
2017-09-26 23:23:04,538 INFO [main] (AbstractApplicationContext.java:583) - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@7ba2a618: startup date [Tue Sep 26 23:23:04 BOT 2017]; root of context hierarchy 2017-09-26 23:23:04,655 INFO [main] (XmlBeanDefinitionReader.java:317) - Loading XML bean definitions from class path resource [com/xgj/dao/transaction/dbConnleak/conf_conn_leak.xml] 連接數[active:idle]-[0:0] 連接數[active:idle]-[0:0] 2017-09-26 23:23:07,759 INFO [Thread-1] (JdbcStudentService.java:40) - addStudent successfully 連接數[active:idle]-[1:1] 連接數[active:idle]-[2:1] 2017-09-26 23:23:09,504 INFO [Thread-2] (JdbcStudentService.java:40) - addStudent successfully 連接數[active:idle]-[2:1]有事務上下文時,需要等到整個事務方法(即 addStudent())返回后,事務上下文綁定的連接才釋放。但在沒有事務上下文時,addStudent() 調用 JdbcTemplate 執行完數據操作后,馬上就釋放連接。
為了避免這種情況,需要進行如下改造
public void addStudent(Student student) {Connection conn = null;try {// 在非事務環境下,通過DataSourceUtils獲取數據連接conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());jdbcTemplate.update(addStudentSQL, student.getName(),student.getAge(), student.getSex());Thread.sleep(1000);// (0-1)模擬程序代碼的執行時間logger.info("addStudent successfully");// (1)} catch (Exception e) {e.printStackTrace();} finally {// 必須顯式使用DataSourceUtils釋放連接,否則造成了解泄露DataSourceUtils.releaseConnection(conn,jdbcTemplate.getDataSource());}}顯式調用 DataSourceUtils.releaseConnection() 方法釋放獲取的連接。特別需要指出的是:一定不能在 (1)處釋放連接!因為如果 addStudent() 在獲取連接后,(1)處代碼前這段代碼執行時發生異常,則(1)處釋放連接的動作將得不到執行。這將是一個非常具有隱蔽性的連接泄漏的隱患點。
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.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="${jdbc.driverClassName}"p:url="${jdbc.url}" p:username="${jdbc.username}" p:password="${jdbc.password}" /><!-- ①對數據源進行代理--> <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 的代理,而且方法存在事務上下文,那么最開始的代碼也不會生產連接泄漏的問題。
其它數據訪問技術的等價類
Spring 為每個數據訪問技術框架都提供了一個獲取事務上下文綁定的數據連接(或其衍生品)的工具類和數據源(或其衍生品)的代理類。
DataSourceUtils 的等價類
| SpringJDBC/ MyBatis | org.springframework.jdbc.datasource.DataSourceUtils |
| Hibernate | org.springframework.orm.hibernateX.SessionFactoryUtils |
| JPA | org.springframework.orm.jpa.EntityManagerFactoryUtils |
| JDO | org.springframework.orm.jdo.PersistenceManagerFactoryUtils |
TransactionAwareDataSourceProxy 的等價類
| SpringJDBC/MyBatis | org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy |
| Hibernate | org.springframework.orm.hibernateX.LocalSessionFactoryBean |
| JPA | org.springframework.orm.jpa.EntityManagerFactoryUtils |
| JDO | 無 |
總結
使用 Spring JDBC 時如果直接獲取 Connection,可能會造成連接泄漏。為降低連接泄漏的可能,盡量使用 DataSourceUtils 獲取數據連接。也可以對數據源進行代理,以便將其擁有事務上下文的感知能力;
可以將 Spring JDBC 防止連接泄漏的解決方案平滑應用到其它的數據訪問技術框架中
示例源碼
代碼已托管到Github—> https://github.com/yangshangwei/SpringMaster
總結
以上是生活随笔為你收集整理的Spring JDBC-数据连接泄露解读的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring JDBC-混合框架的事务管
- 下一篇: Spring JDBC-使用Spring