數據庫連接泄漏是件可怕的事情,可能直接導致系統停止響應,另外因事務管理錯誤而導致數據出現不一致也是件可怕的事情,在后臺系統中,這兩個問題經常會結伴出現,本文將通過案例詳解使用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的問題,創建新線程時的事務問題。最后希望這篇有點繞的文章能給你帶來幫助。