最近看了幾篇關于數據庫事物的文章,很受啟發。對于復雜的業務系統,通常需要操作各種不同的數據源,為了保證數據的統一性和完整性,必須解決數據庫的事物問題。對于簡單的單數據源的事物,利用JDBC的事物控制就可以完成,跨數據源多庫的事物需要利用JTA的API完成。
1、 問題描述
涉及Web的應用,通常針對每次請求,Servlet容器產生一個新的線程。常用的DAO模式,對于每次dao操作都會獲得一個數據庫連接,操作完成后銷毀數據庫連接資源。一個最原始的DAO模式代碼如下:
publicvoid delete(Long id) throws AccountException
{
Connection conn =null;
Statement stmt = null;
ResultSet rs = null;
try {
conn =DbUtil.openConn();
stmt =
conn.createStatement();
stmt.executeQuery("DELETE
FROM TABLE_NAME");
// while(rs.next()){
//...
// }
} catch (Exception e) {
e.printStackTrace();
//...
}finally{
DbUtil.close(conn,
stmt, rs);
}
}
|
這種模式的問題很多,主要有以下幾點:
l
每次調用都會初始化數據庫連接資源,使用完畢后清除資源。對于數據庫資源的創建和銷毀都會消耗大量的性能。
l
使用了JDBC默認的數據庫事物提交方式,調用即提交數據庫事物。如果在該DAO函數里面完成多個操作的話,無法保證事務性。
l
WEB層或者SERVICE層調用DAO方法時,如果一次需要調用多個DAO,那么會多次初始化和銷毀數據庫資源,事物也無法保證。
總結上面的問題,可以發現,其實這種常用的DAO模式根本無法滿足企業系統開發的需要,無法保證系統的性能和數據的完整性。那么,怎么樣的DAO設計才可以滿足企業開發的需要?
2、 性能問題的解決
對于上面的示例,性能的優化主要在于數據庫資源的重用。考慮數據庫連接這種重量級的資源的寶貴性,我們可以建立一個緩存池,對連接進行池化操作。Pool的概念在JAVA EE的設計中經常遇到,連接池的使用可以避免大量不需要的創建和銷毀產生的性能壓力。
基于上面的分析,我們可以設計一個連接池,設置最大連接數量,在系統啟動時初始化連接池。當然,我們可以分別設計一個使用中的連接池和一個空閑的連接池,存放不同狀態的連接。當一個連接被請求獲取,那么它進入InUse狀態,當請求操作完成,該連接進入Idlesse狀態。當然,Idlesse狀態的連接被請求獲取后會進入InUse狀態。
3、 事物問題的控制
一般事物控制有兩種情況,一種是聲明式的事物,如EJB、Spring的AOP等;另外一種是采用直接編碼控制事物。聲明式的事物一般是采用AOP設置代理類,最終和編碼的方式在本質上是一致的。這里我們主要討論編碼式的事物控制。在實際的開發中,可能會跨數據庫處理事物,需要利用JTA控制,這里先考慮簡單的單個數據源的事物控制,主要利用JDBC實現。
對于Web應用的事物控制,核心的問題是事物應該被確定在那個層面上進行。一個最常見的解決方案是統一在Service層處理,比如Spring整合Hibernate時利用AOP在Service層控制事物。事物控制在Service層遇到的問題是,當Controller層需要處理一個復雜的業務時,可能會調用ServiceA和ServiceB,這時,兩個Service的事物沒有辦法保證一致。簡單的解決辦法是多寫一個ServiceC類,在ServiceC中完成復雜的業務功能。并且,在ServiceC中,是不能調用ServiceA或者ServiceB的,否則會產生事物的嵌套。當業務越來越復雜,代碼的利用率會降低。有人提出的解決方案是將Service分類,分為A、B、C三大類。
A類為簡單的操作類Service,即不需要調用DAO函數,不需要操作數據庫,這類Service可以在名稱后面加上后綴區分,如AccountHelper。這類Service就是簡單的輔助對象,可以在任何地方調用。
B類為需要操作DAO函數的,我們直接命名為AccountTransaction。這類Service直接提供給Controller調用。這類Service需要復雜打開和關閉Conn,即簡單的理解為conn.begin()和conn.commit()就位于這類Service的開始和結束,其他的Service對象不能嵌套的調用他們。
C類為需要調用DAO函數的,但是他們不能直接被Controller調用,他們不負責事物的begin和commit。他們只能被B類調用,由B類負責事物控制。
當Service被分解為三大類后,你可以使用Spring的AOP功能配置他們的事物控制代理了。這種方案可以很好的解決事物問題。但是,這種方案的可操作性還是有問題,面對換了一撥又一撥的開發人員,代碼的規范和質量不能很好的保證,那么只能實現一種更加簡單可行的方案了。
當一個Web請求被響應,Servlet會產生一個新的線程,我們可以利用這種唯一的ThreadId作為唯一鍵標記一個Connection,所有基于該請求的操作都利用該Conn完成,事物的控制全部交給該Conn。即我們將事物控制從Service層提到Controller層。我們對Struts的配置文件做一點變通,對于每個Action加入一個關于事物控制的屬性。當不涉及數據庫操作的請求,我們設置為事物無關的,設計數據庫操作的請求設為事物控制的,稍稍減少一下無數據庫操作對事物控制的浪費。考慮下一個需要數據庫操作的Action被請求后,后臺是如何運作的。
首先,取得當前的ThreadId,在連接池中找到一個空閑的Conn,然后把它放置到InUsed池內,同時begin一個事物。Service類不需要什么特殊的修改,不管多少次的嵌套,不管操作多少次的DAO,當需要數據庫操作時,我們利用當前的ThreadId在InUsed池內取得Conn。經歷了N多操作后,在Action負責轉向之前,我們利用當前的ThreadId得到了Conn,我們進行commit。或者在任何一個Exception,我們rollback。當然有一點小小的補充,當類似批處理的請求被提交,我們需要完成大量的工作,可能需要分批次的提交事物。比如,我們要做一個百萬數據導入另外一個表中,我們不能因為9999999條失敗了而回滾了整個事物。我們可以提供一些額外的API,完成個性化的事物控制,將大事物分解為小的事物,分批提交。
上面的文字,完成了對一個簡單的JDBC事物控制的解決方案。對于復雜的業務系統,犧牲部分性能,將事物交給Controller層處理,會比Service層處理得到更加好的開發效率和事物保證。且不論開發人員的素質問題,在利用Spring的AOP功能時,同樣會N多AOP代理的性能消耗。而Controller控制事物,開發人員可以完全不予理會事物操作,不會有嵌套事物,不需要命名規則。