1. 程式人生 > >J2EE事務併發控制策略總結

J2EE事務併發控制策略總結

本文結合 hibernate以及JPA標準,對J2EE當前持久層設計所遇到的幾個問題進行總結:

第一:事務併發訪問控制策略

當前J2EE專案中,面臨的一個共同問題就是如果控制事務的併發訪問,雖然有些持久層框架已經為我們做了很多工作,但是理解原理,對於 我們開發來說還是很有用處的。

事務併發訪問主要可以分為兩類,分別是同一個系統事務和跨事務訪問的併發訪問控制,其中同一個系統事務可以採取樂觀鎖以及悲觀鎖策略, 而跨多個系統事務時則需要樂觀離線鎖和悲觀離線鎖。在討論這四種併發訪問控制策略之前,先需要明確一下資料庫事務隔離級別的問題,ANSI標準規定了四個 資料庫事務隔離級別,它們分別是:

讀取未提交(Read Uncommitted) :這是最低的 事務隔離級別,讀事務不會阻塞讀事務和寫事務,寫事務也不會阻塞讀事務,但是會阻塞寫事務。這樣造成的一個結果就是當一個寫事務沒有提交的時候,讀事務照 樣可以讀取,那麼造成了髒讀的現象。

讀取已提交(Read Committed): 採用此種隔離界別的時候,寫事務就會阻塞讀事務和寫事務,但是讀事務不會阻塞讀事務和寫事務,這樣因為寫事務會阻塞讀取事務,那麼從 而讀取事務就不能讀到髒資料,但是因為讀事務不會阻塞其它的事務,這樣還是會造成不可重複讀的問題。

可重複讀(Repeatable Read):

採用此種隔離級別,讀事務會阻塞寫事務,但是讀事務不會阻塞讀事務,但是寫事務會阻塞寫事務和讀事務。因為讀事務阻塞了寫事務,這樣 以來就不會造成不可重複讀的問題,但是這樣還是不能避免幻影讀問題。

序列化(serializable) :此種隔離級 別是最嚴格的隔離級別,如果設定成這個級別,那麼就不會出現以上所有的問題(髒讀,不可重複讀,幻影讀)。但是這樣以來會極大的影響到我們系統的效能,因 此我們應該避免設定成為這種隔離級別,相反的,我們應該採用較低的隔離界別,然後再採用併發控制策略來進行事務的併發訪問控制)。

其實我們也可以把事務隔離級別設定為serializable,這樣就不需要採用併發控制策略了,資料庫就會為我們做好一切併發控制, 但是這樣以來會嚴重影響我們系統的伸縮性和效能,所以在實踐中,我們一般採用讀取已提交或者更低的事務隔離級別,配合各種併發訪問控制策略來達到併發事務 控制的目的。下面總結一下常用的控制策略:

樂觀鎖

樂觀鎖是在同一個資料庫事務中我們常採取的策略,因為它能使得我們的系統保持高的效能的情況下,提高很好的併發訪問控制。樂觀鎖,顧名 思義就是保持一種樂觀的態度,我們認為系統中的事務併發更新不會很頻繁,即使衝突了也沒事,大不了重新再來一次。它的基本思想就是每次提交一個事務更新 時,我們想看看要修改的東西從上次讀取以後有沒有被其它事務修改過,如果修改過,那麼更新就會失敗,。

最後我們需要明確一個問題,因為樂觀鎖其實並不會鎖定任何記錄,所以如果我們資料庫的事務隔離級別設定為讀取已提交或者更低的隔離界 別,那麼是不能避免不可重複讀問題的(因為此時讀事務不會阻塞其它事務),所以採用樂觀鎖的時候,系統應該要容許不可重複讀問題的出現。

瞭解了樂觀鎖的概念以後,那麼當前我們系統中又是如何來使用這種策略的呢?一般可以採用以下三種方法:

版本(Version)欄位: 在我們的實體中增加一個版本控制欄位,每次事務更新後就將版本欄位的值加1.

     時間戳(timestamps): 採取這種策 略後,當每次要提交更新的時候就會將系統當前時間和實體載入時的時間進行比較,如果不一致,那麼就報告樂觀鎖失敗,從而回滾事務或者重新嘗試提交。採用時 間戳有一些不足,比如在叢集環境下,每個節點的時間同步也許會成問題,並且如果併發事務間隔時間小於當前平臺最小的時鐘單位,那麼就會發生覆蓋前一個事務 結果的問題。因此一般採用版本欄位比較好。

基於所有屬性進行檢測: 採用這種策略的時候,需要比較每個欄位在讀取以後有沒有被修改過,所以這種策略實現起來比較麻煩,要求對每個屬性都進行比較,如果採 用hiernate的話,因為hibernate在一級快取中可以進行髒檢測,那麼可以判斷哪些欄位被修改過,從而動態的生成sql語句進行更新。

下面再總結一下如何在JDBC和Hibernate中使用樂觀鎖

JDBC中使用樂觀鎖: 如果我們採用JDBC來實現持久層的話,那麼就可以採用以上將的三種支援樂觀鎖的策略,在實體中增加一個version欄位或者一個 Date欄位,也可以採用基於所有屬性的策略,下面就採用version欄位來做一演示:

假如系統中有一個Account的實體類,我們在Account中多加一個version字 段,那麼我們JDBC Sql語句將如下寫:

Select  a.version....from Account  as a where (where condition..)

Update Account set version = version+1.....(another field) where version =?...(another contidition)

 

這樣以來我們就可以通過更新結果的行數來進行判斷,如果更新結果的行數為0,那麼說明實體從 載入以來已經被其它事務更改了,所以就丟擲自定義的樂觀鎖定異常(或者也可以採用spring封裝的異常體系)。具體例項如下:

.......

int rowsUpdated = statement.executeUpdate(sql);

If(rowsUpdated= =0){

throws new OptimisticLockingFailureException();

}

........

 

在使用JDBC API的情況下,我們需要在每個update語句中,都要進行版本欄位的更 新以及判斷,因此如果稍不小心就會出現版本欄位沒有更新的問題,相反當前的ORM框架卻為我們做好了一切,我們僅僅需要做的就是在每個實體中都增加 version或者是Date欄位。

 

Hibernate中使用樂觀鎖: 如果我們採用hibernate做為持久層的框架,那麼實現樂觀鎖將變得非常容易,因為框架會幫我們生成相應的sql語句,不僅減少 了開發人員的負擔,而且不容易出錯。下面同樣採用version欄位的方式來總結一下:

同樣假如系統中有一個Account的實體類,我們在Account中多加一個 version欄位,

public class Account{

Long id ;

.......

@Version  //也可以採用XML檔案進行 配置

Int version

.......

 

}

 

這樣以來每次我們提交事務時,hibernate內部會生成相應的SQL語句將版本欄位加 1,並且進行相應的版本檢測,如果檢測到併發樂觀鎖定異常,那麼就丟擲StaleObjectStateException.

 

悲觀鎖

所謂悲觀鎖,顧名思義就是採用一種悲觀的態度來對待事務併發問題,我們認為系統中的併發更新會非常頻繁,並且事務失敗了以後重來的開銷 很大,這樣以來,我們就需要採用真正意義上的鎖來進行實現。悲觀鎖的基本思想就是每次一個事務讀取某一條記錄後,就會把這條記錄鎖住,這樣其它的事務要想 更新,必須等以前的事務提交或者回滾解除鎖。

最後我們還是需要明確一個問題,假如我們資料庫事務的隔離級別設定為讀取已提交或者更低,那麼通過悲觀鎖,我們控制了不可重複讀的問 題,但是不能避免幻影讀的問題(因為要想避免我們就需要設定資料庫隔離級別為Serializable,而一般情況下我們都會採取讀取已提交或者更低隔離 級別,並配合樂觀或者悲觀鎖來實現併發控制,所以幻影讀問題是不能避免的,如果想避免幻影讀問題,那麼你只能依靠資料庫的serializable隔離級 別(幸運的是幻影讀問題一般情況下不嚴重)。

下面就分別以JDBC和hibernate來總結一下:

JDBC中使用悲觀鎖: 在JDBC中 使用悲觀鎖,需要使用select for update語句,假如我們系統中有一個Account的類,我們可以採用如下的方式來進行:

Select * from Account where ...(where condition).. for update.

當使用了for update語句後,每次在讀取或者載入一條記錄的時候,都會鎖住被載入的記錄,那麼當其他事務如果要更新或者是載入 此條記錄就會因為不能獲得鎖而阻塞,這樣就避免了不可重複讀以及髒讀的問題,但是其他事務還是可以插入和刪除記錄,這樣也許同一個事務中的兩次讀取會得到 不同的結果集,但是這不是悲觀鎖鎖造成的問題,這是我們資料庫隔離級別所造成的問題。

最後還需要注意的一點就是每個衝突的事務中,我們必須使用select for update 語句來進行資料庫的訪問,如果一些事務 沒有使用select for update語句,那麼就會很容易造成錯誤,這也是採用JDBC進行悲觀控制的缺點。

Hibernate中使用悲觀鎖: 相比於JDBC使用悲觀鎖來說,在hibernate中使用悲觀鎖將會容易很多,因為hibernate有API讓我們來呼叫,從而 避免直接寫SQL語句。下面就hibernate使用悲觀鎖做一總結:

首先先要明確一下hibernate中支援悲觀鎖的兩種模式 LockMode.UPGRADE以及LockMode.UPGRADE_NO_WAIT.(PS:在JPA中,對應的鎖模式是 LockModeType.Read,這與hibernate是不一樣的呵呵)

假如我們系統中有一個Account的類,那麼具體的操作可以像這樣:

.......

session.lock(account, LockMode.UPGRADE);

......

 

或者也可以採用如下方式來載入物件:

session.get(Account.class,identity,LockMode.UPGRADE).

 

這樣以來當載入物件時,hibernate內部會生成相應的 select for update語句來載入物件,從而鎖定對應的記錄,避免其它事務併發更新。

 

以上兩種策略都是針對同一個事務而言的,如果我們要實現跨多個事務的併發控制就要採用其它兩種併發控制策略了,下面做一總結:

樂觀離線鎖

樂觀離線鎖的思想和樂觀鎖是一致的,不同的地方就是樂觀鎖是針對一個事務週期的(也可以說是一個request週期),而樂觀離線鎖是 跨多個事務週期的(可以理解為一個會話週期(conversation),它包括了使用者思考的時間。

因為樂觀離線鎖是跨多個事務週期的,那麼我們就遇到了一個問題,在幾個事務之間通過什麼來儲存我們的實體狀態呢?當然我們會想到 httpsession,知道了這個,我們又會遇到一個問題,我們是通過業務層來判斷有沒有發生樂觀併發異常還是通過持久層框架來實現(因為目前持久層框 架一般都有一個脫管(detached object)的概念,我們通過它可以很容易實現樂觀離線鎖),下面就以上兩種方式分別做一總結(假設持久層框架 採用hibernate):

在進行總結之前,先讓我們來選個場景來描述,因為最近在網上投了幾份簡歷,我發現投簡歷之後,在HR處理簡歷之前,我們可以修改簡歷, 我覺得這個例子就很符合樂觀理線鎖的問題域,所以我就以它來分析一下。我們先假設系統中有一個簡歷類Resume(因為只是一個例子,為了讓問題更加清 楚,我將其它的屬性省略):

 

 

public class Resume implements Serializable{

public long id ;

 

.............

 

public int version ;

 

............

 

}

業務層負責判斷樂觀併發異常: 在此種情況下,我們一般需要在Resume中增加一個獲取欄位的getVersion()方法,因為我們要將獲得的版本欄位值儲存到 Httpsession裡面,首先第一個請求過來,我們獲得版本欄位將其儲存到httpsession中,然後當用戶修改完簡歷後打算更新的時候,那麼我 們的業務層怎麼實現呢?可以如下:

  ..............

Resume resumeToModify = resumeRepository.getResume(id);

If(beforeModifyVersion == resumeToModify.getVersion()){

//這裡進行修改操作

}else 

throw new OptimisticLockFailureException();

 

這樣我們就可以通過對比httpsession中的版本欄位來和當前資料庫系統中的欄位進行比較,如果一樣就提交,否則就丟擲異 常。(呵呵,當然這個僅僅是作為例子,其實如果你修改了很大一會簡歷,但是發現提交的時候不能完成,那你應該會很鬱悶,覺得這網站做的不人性化,然而這是 採用樂觀離線鎖不能避免的,所以這也就出現了使用樂觀離線鎖的一個前提:系統中併發更新少,並且即使衝突了,讓使用者重來一遍的開銷也不會很大,如果開銷大 的話,那麼我們就需要採用悲觀離線鎖模式)。

通過detached object實現: 如果通過持久層提供的脫管物件來實現,那麼我們就可以依賴於持久層框架的樂觀檢測來實現樂觀離線鎖,這樣我們不需要在業務層進行比 較,持久層框架會幫我們進行比較,如果發現數據已經被更改過,那麼就會丟擲相應的異常(比如hibernate將丟擲 StaleObjectStateExcpetion)。我們還是拿剛才的那個例子來做一描述,當用戶要修改簡歷時,第一個請求會將簡歷的內容讀取並顯示 給使用者,我們需要做的就是將脫離了持久層的脫管物件儲存到httpsession中,然後當用戶修改完以後,要提交更新的時候,我們可以如下實現:

............

resumeRepository.update(detachedResume);//內 部通過呼叫hibernate session介面的update方法,將脫管物件重附(reattch)到當前的持久化上下文

.............

 

這樣以來,當持久層框架發現數據已經被修改後就會丟擲相應的異常,我們的業務層只要捕獲異常 就OK啦。

好了,總結完了兩種方式以後,我們來看看它們有哪些優缺點:

對於第一種策略,因為需要們顯示的來判斷到底有沒有出現異常,所以略顯麻煩,但是優點就是因 為我們只需要儲存一個版本欄位,不會出現將整個物件都儲存到httpsession中,這樣造成httpsession被充爆。如果採用第二種策略 (detached object),不需要我們顯示來實現,只需要捕獲持久層丟擲的異常就OK,但是也有個不好地方,前面也說了,那就是會造成我們 httpsession爆炸,這樣很浪費記憶體。再說點題外話,既然httpsession會引起記憶體浪費太多,那麼有沒有更好的方案呢?當然我們會想到 EJB的有狀態會話bean,它為業務狀態提供了鈍化技術,所以可以避免佔用太大的記憶體,並且它還有個更好的優點就是不僅有狀態管理,而且還支援完整的事 務語義,所以EJB的statefull session bean還是有很大好處的,並不像有些人所說的 statefull session bean 用處非常小。

悲觀離線鎖

悲觀離線鎖的基本思想和離線鎖差不多,不同的地方就是悲觀鎖是依賴於資料庫的鎖機制來實現,而悲觀離線鎖則需要我們開發人員自己來控制 鎖的釋放和獲取,所以增加了一定的複雜性。

下面需要明確一下什麼情況下需要使用悲觀離線鎖,因為悲觀離線鎖會在整個會話期間鎖住資料,所以會影響到系統的伸縮性和效能,選擇時要 謹慎考慮幾方面的因素:

 1 用例是否通過幾個數據庫事務來實現,第一個事務讀取資料,最後一個事務更新資料,首先要滿足這個條件我們才會選擇用悲觀離線鎖,否則 如果在同一個資料庫事務,直接用離線鎖即可。

 2  用例是否要求使用者提交的更新一定要成功,以及使用者提交事務後重新來一次的開銷大不大,如果要求使用者提交更新一定要成功,或者使用者提交 更新失敗後,再重新來一次的開銷比較大,那麼就採取悲觀離線鎖,否則的話,我們可以採用樂觀離線鎖。

原文地址:http://xmuzyq.javaeye.com/blog/295639