spring+hibernate连接泄漏之殇

Posted on 2012-11-07 16:32 terryxue 阅读(2269) 评论(0)  编辑  收藏 所属分类: java
数据库连接泄漏是件可怕的事情,可能直接导致系统停止响应,另外因事务管理错误而导致数据出现不一致也是件可怕的事情,在后台系统中,这两个问题经常会结伴出现,本文将通过案例详解使用Spring+Hibernate时可能导致问题的几种情况,希望对大家有所帮助。

文章比较长,如果你是遇到了问题正急求解决方案的话,可以先只读这一条:检查你的dao是否有直接使用session,修改为使用hibernateTemplate。
如果你希望更多的了解session, hibernateTemplate, transaction,请继续。

以下案例基于Struts2.3.1+Spring3.1.1+hibernate3.5.4完成,案例场景为对系统参数进行管理,涉及如下四个类:
Action PropertyAction
Service PropertyService/PropertyServiceImpl
DAO PropertyDAO(继承HibernateDaoSupport)
Entity SysProperty

1. Action中直接调用dao,未使用getHibernateTemplate
在一个复杂的多层架构系统中,事务控制在service中完成是更合理的,不应交由view层来控制,本文假定你是这么做的。一般我们代码的处理顺序为action->service->dao,然而总会有人会打破这种逻辑,打破可以,但要注意如下问题。
假定Action中有如下代码:
1 @Autowired
2 private PropertyDAO propertyDAO;
3 @Override
4 public String execute() throws Exception {
5      model = propertyDAO.get(1L);
6      return null;
7 }

 这里action直接访问了dao中的get方法,因事务配置在service层,因此这里是没有事务控制的。
接下来我们看dao中的get方法,假定你的定义如下:
1 public SysProperty get(Long id) {
2     return (SysProperty) getSession().get(SysProperty.class, id);
3 }

代码很简单,getSession是父类HibernateDaoSupport中提供的方法,这里直接通过实体Id get得到结果。
接下来发布一下系统,访问页面,好像一切OK,但是,再刷新几下看看,可能你的连接池配置的比较大,要刷新多次,最后你发会现你的系统停止了响应。更直接点,调用你采用的连接池中的相关API检测下当前连接占用情况,你会发现刷新一次,被占用连接增加一个,可怕的事情发生了。
小结:在无事务管理器控制的情况下,通过getSession()打开的session不会被关闭,这个session所占用的数据库连接也不会被释放。

接下来,来点更可怕的,假设你的action中是这样的:
1 private List<Long> ids;
2 @Override
3 public String execute() throws Exception {
4   for(Long id: ids) {
5     results.add(propertyDAO.get(id));
6   }
7   return null;
8 }
9 
这时尝试一次传入多个id进行请求,请求结束后检查下连接数,你发现传入多少ID,就有多少连接被占用,也就是说每一次对dao的get调用都会占用一个不可释放的连接。
小结:如果没有配置事务管理器且直接使用getSession得到session,每次getSession都会创建一个新的session,每个session占用一个新的数据库连接,session无线程级别的共享。

2. Action中直接调用dao,使用getHibernateTemplate
当然,你可能没遇到过前面的情况,因为你会在DAO中这样写代码:
1 public SysProperty get(Long id) {
2     return getHibernateTemplate().get(getEntityClass(), id);
3 }
那么是否就一切OK呢?如果只是想解决连接泄漏这个问题的话,答案是Yes,尝试多次请求,检查下你的连接池状况,没有连接泄漏的情况出现。所以简单而可靠的办法就是不再直接使用getSession,而是使用getHibernateTemplate进行相应操作,当需要使用session时,可以用下面的方法:
getHibernateTemplate().execute(new HibernateCallback<T>() {
  
public T doInHibernate(Session session) throws HibernateException {
    Query queryObject 
= session.createQuery(hql);
    //....
  }
});
But...如果你的系统真的出现了连接泄漏,可能你需要关注更多。
还是前面Action中根据ID循环查询的操作,在这个案例中,每一次Dao的get方法都将重复这样的逻辑:创建session,分配session一个连接,关闭session,释放session占用的连接,也就是说session不会在线程级别共享。让我们继续,进入第三种情况来进一步说明。

3. Service未合理配置事务管理器
绝大部分情况下我们会给service配置事务管理器,可能是通过xml或是@Transactional注解,但并非配置了就OK了。看下面的例子:
@Transactional
public
 class PropertyServiceImpl implements PropertyService {
    @Autowired
    
private PropertyDAO propertyDAO;
    
    @PostConstruct
    
public void init(){
        propertyDAO.get(
1L);
    }
    
//
}
当然,假定你的DAO还是写成了这样:
public SysProperty get(Long id) {
    
return (SysProperty) getSession().get(SysProperty.class, id);
}
你期望在service初始化好后做一些数据库操作,你也给service配置了事务管理器,接下来你启动应用,然后检查连接池中的连接数,你会发现有连接未被释放!可能你会果断的修改dao中的方法:
public SysProperty get(Long id) {
    
return getHibernateTemplate().get(getEntityClass(), id);
}
然后你会理所当然的认为,即使配置了事务管理器,依然不能使用getSession(),事实可能并非如此。
我们调整一下代码,不在init中调用dao中的get方法,改为如下:
public SysProperty getProperty(Long id) {
    
return propertyDAO.get(id);
}
然后DAO继续使用(SysProperty) getSession().get(SysProperty.class, id),而action中的调用修改为:
@Autowired
private PropertyService propertyService;
@Override
public String execute() throws Exception {
    model 
= propertyService.getProperty(1L);
    
return null;
}
重新发布,调用,检查连接数,发现无被占用连接存在。
小结:如果正确配置了事务管理器,getSession是安全的。

这时要清楚spring是通过AOP来实现事务控制的,而@PostConstruct方法不会受AOP控制,因此上面的init方法等于无事务管理器。
那么再回头来说,是否只要dao中使用getHibernateTemplate就不会有问题呢?
假定service中的@PostConstruct方法如下:
@PostConstruct
public void init(){
    SysProperty p 
= new SysProperty();
    p.setName(
"test_property_1");
    propertyDAO.save(p);
}
强调一下,前面已经提到这个方法不受spring的事务管理器控制。
假定DAO中的save方法如下:
public void save(SysProperty o){
    getSession().save(o);
}
启动一下应用,检查一下连接数,再检查一下数据是否有存储到数据库中。因为我们直接使用了getSession,因此连接不会释放,这点前面已经提到,但同时我们还将发现数据没有被存储,当然这个也好理解,因为上面已经提到这个方法未配置事务管理器。
小结:通过getSession().save()保存数据时,事务不会自动提交。

现在再修改下DAO中的save方法:
public void save(SysProperty o){
    getHibernateTemplate().save(o);
}
启动一下应用,检查一下连接数,再检查一下数据是否有存储到数据库中。因为我们使用hibernateTemplate,因此连接有释放,这点前面已经提到,但同时我们还发现数据也已存储到数据库中,说明hibernateTemplate会自动提交事务。
小结:如果未配置事务管理器,通过hibernateTemplate操作时,会自动创建并提交事务。

所以如果你觉得使用hibernateTemplate就OK了,那就要小心下面的代码了:
@PostConstruct
public void init(){
  
//1. 从你的账号A中扣除一万块
  
//2. 这里的代码抛出了异常
  
//3. 将你的账号B中增加一万块 
}
如果上面的第2步出现了异常,那么因为1的事务已经提交,而3却没有执行,最终导致了数据的不一致,后果和连接泄漏一样严重!
除了@PostConstruct,还有其它原因会导致@Transactional无效,假定我们的service配置了事务管理器,但存在如下代码:
pubic void someServiceMethod(){
  
new Thread(){
    
public void run(){
        doSomethingLater();
    }
  }
}

public void doSomethingLater(){
  
//做一系列数据库操作
}
那么你可以去验证下doSomethingLater是否受事务管理器控制,事实上并不会,所以你需要理解spring AOP的机制,否则一个小坑会酿成灾难。
这里还有一种情况,你不是在类上面配置@Transactional,而是在方法上面配置,假定存在如下的代码:
//该方法不需要事务控制
public void method1(){
    method2();
}

//下面的方法需要事务控制
@Transactional
public void method2(){
    
//do something
}
因为你需要给不同的方法配置不同的事务机制,因此你没有在类上面进行配置,然后你在客户端进行了如下调用:service.method1(); method2中的方法会受事务管理吗?可悲的是并不会。
上面讲到的线程调用和内部方法调用可以这样来处理:
@Autowired
private ApplicationContext context;  
public void method1(){
    PropertyService service 
= context.getBean(PropertyService.class);
    service.method2();
}
小结:注意spring的AOP机制

4. Service合理配置事务管理器
最后补充一下,如果事务管理器配置正确的话会发生什么。这时不管你是用getSession还是getHibernateTemplate,结果都是一样,session将在thread级别共享,session只有一个。

总结:难得这周开始工作变得清闲,上班时间还能写写博客,想想前段日子真是自己何苦为难自己。回到话题,使用getSession没什么大错,因为你本应正确配置事务管理器。使用hibernateTemplate能解决所有连接泄漏的问题,但要小心他可能隐藏的事务问题。另外就是spring中内部方法调用时AOP的问题,创建新线程时的事务问题。最后希望这篇有点绕的文章能给你带来帮助。

只有注册用户登录后才能发表评论。


网站导航:
 

posts - 9, comments - 24, trackbacks - 0, articles - 0

Copyright © terryxue