【转】Spring 事务管理高级应用难点剖析: 第 3 部分
概述
对于应用开发者来说,数据连接泄漏无疑是一个可怕的梦魇。如果存在数据连接泄漏问题,应用程序将因数据连接资源的耗尽而崩溃,甚至还可能引起数据库的崩溃。数据连接泄漏像黑洞一样让开发者避之唯恐不及。
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
保证 BasicDataSource 数据源的配置默认连接为 0,运行以上程序代码,在控制台中将输出以下的信息:
清单 4. 输出日志
连接数 [active:idle]-[0:0]
连接数 [active:idle]-[2:0]
连接数 [active:idle]-[1:1]
连接数 [active:idle]-[3:0]
连接数 [active:idle]-[2:1]
我们通过下表对数据源连接的占用和泄漏情况进行描述:
表 1. 执行过程数据源连接占用情况
学习
?
?