B/S構架的應用越來越普及,但由于它有別于C/S構架的特殊性,并發控制始終沒能得到很好的解決,如售票系統經常會出現同一張火車票出售多次的現象。典型的案例如下:
例如若有兩個客戶端,A客戶先讀取了賬戶余額2000元,之后B客戶也讀取了賬戶余額2000元的數據,A客戶提取了500元,對數據庫作了變更,此時數據庫中的余額為1500元,B客戶也要提取1300元,根據其所取得的資料,2000-1300將為700余額,若此時再對數據庫進行變更,最后的余額700元就會不正確,應當是200元,問題的出現是由于兩個客戶對同一條數據進行并發訪問造成的。
Web應用中并發控制的特殊性
上述問題在C/S構架中可以通過長事務來實現,但Web應用是基于Internet網絡環境的,其中的并發控制有其內在的特殊性:
1. Web所基于的網絡協議HTTP(Hyper Text Transfer Protocol)是一種無連接的協議,數據庫服務器無法保存事務的狀態信息;
2. 用戶可以隨時中止或啟動瀏覽器中當前主頁上的事務。
由于上述特殊性,Web應用中并發控制不能采用嚴格的長事務來實現,但可以長事務的思路來實現,在數據讀取的時候把相應的數據鎖定,在更新階段把鎖放開,然后更新數據。
Web應用中并發控制的實現
業務邏輯的實現過程中,往往需要保證數據訪問的排他性。如在 金融 系統的日終結算處理中,我們希望針對某個cut-off時間點的數據進行處理,而不希望在結算進行過程中(可能是幾秒種,也可能是幾個小時),數據再發生變化。此時,我們就需要通過一些機制來保證這些數據在某個操作過程中不會被外界修改,這樣的機制,就是所謂的“鎖”,即給選定的目標數據上鎖,使其無法被其他程序修改。有兩種鎖機制:即通常所說的“樂觀鎖(Optimistic Locking)” 和“悲觀鎖(Pessimistic Locking)”。
1.樂觀鎖(Optimistic Locking)
樂觀鎖(optimistic locking)則樂觀的認為資料的存取很少發生同時存取的問題,因而不作數據庫層次上的鎖定,為了維護正確的數據,樂觀鎖定使用應用程序上的邏輯實現版本控制來解決。
并發控制時,數據不一致的情況一旦發生,有幾個解決的 方法 ,一種是先更新為主,一種是后更新的為主,比較復雜的就是檢查發生變動的數據來實現,或是檢查所有屬性來實現樂觀鎖定。
Hibernate通過版本號檢查來實現后更新為主,這也是Hibernate所推薦的方式,在數據庫中加入一個VERSON欄記錄,在讀取數據時連同版本號一同讀取,并在更新數據時遞增版本號,然后比對版本號與數據庫中的版本號,如果大于數據庫中的版本號則予以更新,否則就回報錯誤。
以Hibernate實現版本號控制鎖定的話,我們的對象中增加一個version屬性,例如:
public class MyAccount {
private int version;
....
public void setVersion(int version) {
this.version = version;
}
public int getVersion() {
return version;
}
....
}
|
而在映像文件中,我們使用optimistic-lock屬性設定version控制,屬性欄之后增加一個標簽,例如:
optimistic-lock="version"
|
設定好版本控制之后,在上例中如果B客戶試圖更新數據,將會引發StableObjectStateException例外,我們可以捕捉這個例外,在處理中重新讀取數據庫中的數據,同時將B客戶目前的數據與數據庫中的數據讀出來,讓B客戶有機會比對不一致的數據,以決定要變更的部份,或者您可以設計程式自動讀取新的資料,并重復扣款業務流程,直到數據可以更新為止,這一切可以在后臺執行,而不用讓您的客戶知道。在其它架構中也可通過這種思路來實現樂觀鎖,但版本控制和沖突的檢測要在自己程序的程序中實現和維護。
2.悲觀鎖(Pessimistic Locking)
雖然樂觀鎖能夠提高系統的性能,但它是對發生沖突的訪問進行事后的補救,應用在用戶輸入數據量很少的場合比較適合,但如果在 企業 ERP,用戶與系統交互涉及大量數據在頁面表單上錄入,如果事后提交失敗后才提示用戶要重新錄入是很不現實的,所以有必要進行事前控制,這就要采用悲觀鎖。
在多個客戶端可能讀取同一筆數據或同時更新一筆數據的情況下,防止同一個數據被修改而造成混亂,最簡單的手段就是在讀取時對數據進行鎖定,其它客戶端不能對同一筆數據進行更新的讀取動作。
悲觀鎖定(Pessimistic Locking)一如其名稱所示,悲觀的認定每次資料存取時,其它的客戶端也會存取同一筆數據,因此對該筆數據進行事先鎖定,直到自己操作完成后解除鎖定。
悲觀鎖定通常透過系統或數據庫本身的功能來實現,依賴系統或數據庫本身提供的鎖定機制,Hibernate即是如此,我們可以利用Query或Criteria的setLockMode()方法來設定要鎖定的表或列(row)及其鎖定模式,鎖定模式有以下的幾個:
LockMode.WRITE:在insert或update時進行鎖定,Hibernate會在save()方法時自動獲得鎖定。
LockMode.UPGRADE:利用SELECT … FOR UPDATE進行鎖定。
LockMode.UPGRADE_NOWAIT:利用SELECT … FOR UPDATE NOWAIT進行鎖定,在Oracle環境下使用。
LockMode.READ:在讀取記錄時Hibernate會自動獲得鎖定。
LockMode.NONE:沒有鎖定。
也可以在使用Session的load()或是lock()時指定鎖定模式以進行鎖定。
如果數據庫不支持所指定的鎖定模式,Hibernate會選擇一個合適的鎖定替換,而不是丟出一個例外。
3.其它構架中悲觀鎖的實現
Hibernate的悲觀鎖,也是基于數據庫的鎖機制實現。下面的代碼實現了對“用戶”查詢記錄的加鎖:
String sqlStr = "from userInfo as user where user.userId=’admin’"; Query query = session.createQuery(sqlStr); query.setLockMode("user",LockMode.UPGRADE); //加鎖 List userList = query.list();//執行查詢,獲取數據
|
query.setLockMode對查詢語句中,特定別名所對應的記錄進行加鎖(我們為userInfo類指定了一個別名“user”),這里也就是對返回的所有user記錄進行加鎖:
select tuser0_.id as id, tuser0_.userId as userId, tuser0_.group_id as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex from t_user tuser0_ where (tuser0_.userId =’admin’ ) for update
|
通過上述轉換后的sql語句可知,Hibernate的加鎖其實是利用了數據庫的for update語句,在讀取階段對某條記錄的鎖定,而在更新階段提交,釋放鎖。
其實其它架構也可以采取該思路,不過,數據庫的for update語句的鎖定和釋放一定要在數據的同一個連接中,如果讀取階段和更新階段不是統一連接,即讀取之后斷開了與數據庫的連接,則for update語句的鎖定立即失效,為此,如果其它架構中要采取這種方式則要做相應的調整。
首先,由于Web應用是無狀態的,也就是說數據庫的for update語句的鎖定和釋放不一定是數據的同一個連接,為此,采用痕跡跟蹤法,在讀取數據時生成唯一的序列號(serialId),建立與數據連接的映射,并放置一個map數據結構中;在更新時,通過該serialId在連接池中重新獲取該連接,用該連接去更新數據。
如果系統是采用dao讀取數據,實體bean去更新數據,則只要在更新數據之前斷開讀取數據時的連接,則可以通過其它途徑更新數據,如下代碼所示:
public void update (AbstractEntityData data, String[] selTeamName ,String serialId) throws Exception { dao.closeConnect(serialId); bo.update(data); }
|
其中,dao.closeConnect(serialId)是斷開數據連接,bo.update(data)是通過EJB更新數據庫
4.序列號(serialId)的創建和維護
由于不同用戶可能同時建立連接或同一用戶先后建立連接,故創建序列號可以在讀取數據時通過sessionId和時間戳組合而成。而在操作的過程中,為了保持序列號不會丟失和唯一性,它不能放在session或application中,而是放在頁面的request對象里,通過它向其它頁面傳遞。
5.關聯表的鎖定
其實,Hibernate的悲觀鎖方式只能對單個表的記錄進行鎖定,但現實中,存在關聯更新的情況,即在更新主表的時候有可能會更新到與之相關的子表,與此同時,其它用戶也可能通過其它主表更新相應的子表同一條記錄。
有兩種方式處理,一是在讀取數據通過sql語句關聯子表相應記錄,因為for update對所有關聯表中符合條件的記錄都會加鎖;二是為子表找一個入口表,在更新子表的同時,必須更新子表的入口表。
6.例外操作的處理
采用這種方式,有一些例外情況必須小心處理,一是頁面的關閉,如果調用相應的方法,如onbeforeunload()等,釋放對應的數據庫連接;二是用戶非正常關機退出系統,必須有數據庫周期清除無用的連接,如間隔二十分鐘等,來釋放讀取時對數據的鎖定,否則,該數據會長時間被鎖定,直至應用服務器重啟。
結論
軟件系統的并發控制一般是通過加鎖來實現,同樣,Web應用也是采用樂觀鎖和悲觀鎖來實現,樂觀鎖是一種事后補救措施,是通過程序的邏輯控制版本來實現的,而悲觀鎖是事前的一種預防措施,它利用數據庫的鎖機制來實現,Hibernate對它做了一層封裝,使應用更加方便,為了讓其它架構都能適用,本文還原了Hibernate的實現原理,提出一般的實現思路和注意實現。