数据库连接泄漏是件可怕的事情,可能直接导致系统停止响应,另外因事务管理错误而导致数据出现不一致也是件可怕的事情,在后台系统中,这两个问题经常会结伴出现,本文将通过案例详解使用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的问题,创建新线程时的事务问题。最后希望这篇有点绕的文章能给你带来帮助。