J2EE事務並發控制策略總結

來源:互聯網
上載者:User

事務並發存取控制策略
  當前J2EE項目中,面臨的一個共同問題就是如果控制事務的並發訪問,雖然有些持久層架構已經為我們做了很多工作,但是理解原理,對於我們開發來說還是很有用處的。
  事務並發訪問主要可以分為兩類,分別是同一個系統事務和跨事務訪問的並發存取控制,其中同一個系統事務可以採取樂觀鎖以及悲觀鎖策略,而跨多個系統事務時則需要樂觀離線鎖和悲觀離線鎖。在討論這四種並發存取控制策略之前,先需要明確一下資料庫交易隔離等級的問題,ANSI標準規定了四個資料庫交易隔離等級,它們分別是:
  讀取未提交(Read Uncommitted)
  這是最低的交易隔離等級,讀事務不會阻塞讀事務和寫事務,寫事務也不會阻塞讀事務,但是會阻塞寫事務。這樣造成的一個結果就是當一個寫事務沒有提交的時候,讀事務照樣可以讀取,那麼造成了髒讀的現象。
  讀取已提交(Read Committed)
  採用此種隔離界別的時候,寫事務就會阻塞讀事務和寫事務,但是讀事務不會阻塞讀事務和寫事務,這樣因為寫事務會阻塞讀取事務,那麼從而讀取事務就不能讀到髒資料,但是因為讀事務不會阻塞其它的事務,這樣還是會造成不可重複讀取的問題。
  可重複讀(Repeatable Read)
  採用此種隔離等級,讀事務會阻塞寫事務,但是讀事務不會阻塞讀事務,但是寫事務會阻塞寫事務和讀事務。因為讀事務阻塞了寫事務,這樣以來就不會造成不可重複讀取的問題,但是這樣還是不能避免幻影讀問題。
  序列化(serializable)
  此種隔離等級是最嚴格的隔離等級,如果設定成這個層級,那麼就不會出現以上所有的問題(髒讀,不可重複讀取,幻影讀)。但是這樣以來會極大的影響到我們系統的效能,因此我們應該避免設定成為這種隔離等級,相反的,我們應該採用較低的隔離界別,然後再採用並發控制策略來進行事務的並發存取控制)。
  其實我們也可以把交易隔離等級設定為serializable,這樣就不需要採用並發控制策略了,資料庫就會為我們做好一切並發控制,但是這樣以來會嚴重影響我們系統的伸縮性和效能,所以在實踐中,我們一般採用讀取已提交或者更低的交易隔離等級,配合各種並發存取控制策略來達到並發事務控制的目的。下面總結一下常用的控制策略:
  1 樂觀鎖
  樂觀鎖是在同一個資料庫事務中我們常採取的策略,因為它能使得我們的系統保持高的效能的情況下,提高很好的並發存取控制。樂觀鎖,顧名思義就是保持一種樂觀的態度,我們認為系統中的事務並發更新不會很頻繁,即使衝突了也沒事,大不了重新再來一次。它的基本思想就是每次提交一個事務更新時,我們想看看要修改的東西從上次讀取以後有沒有被其它事務修改過,如果修改過,那麼更新就會失敗,。
  最後我們需要明確一個問題,因為樂觀鎖其實並不會鎖定任何記錄,所以如果我們資料庫的交易隔離等級設定為讀取已提交或者更低的隔離界別,那麼是不能避免不可重複讀取問題的(因為此時讀事務不會阻塞其它事務),所以採用樂觀鎖的時候,系統應該要容許不可重複讀取問題的出現。
  瞭解了樂觀鎖的概念以後,那麼當前我們系統中又是如何來使用這種策略的呢?一般可以採用以下三種方法:
  版本(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.
  2 悲觀鎖
  所謂悲觀鎖,顧名思義就是採用一種悲觀的態度來對待事務並發問題,我們認為系統中的並發更新會非常頻繁,並且事務失敗了以後重來的開銷很大,這樣以來,我們就需要採用真正意義上的鎖來進行實現。悲觀鎖的基本思想就是每次一個事務讀取某一條記錄後,就會把這條記錄鎖住,這樣其它的事務要想更新,必須等以前的事務提交或者復原解除鎖。
  最後我們還是需要明確一個問題,假如我們資料庫事務的隔離等級設定為讀取已提交或者更低,那麼通過悲觀鎖,我們控制了不可重複讀取的問題,但是不能避免幻影讀的問題(因為要想避免我們就需要設定資料庫隔離等級為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語句來載入對象,從而鎖定對應的記錄,避免其它事務並發更新。
  以上兩種策略都是針對同一個事務而言的,如果我們要實現跨多個事務的並發控制就要採用其它兩種並發控制策略了,下面做一總結:

3 樂觀離線鎖 樂觀離線鎖的思想和樂觀鎖是一致的,不同的地方就是樂觀鎖是針對一個事務周期的(也可以說是一個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 用處非常小。 4 悲觀離線鎖悲觀離線鎖的基本思想和離線鎖差不多,不同的地方就是悲觀鎖是依賴於資料庫的鎖機制來實現,而悲觀離線鎖則需要我們開發人員自己來控制鎖的釋放和擷取,所以增加了一定的複雜性。下面需要明確一下什麼情況下需要使用悲觀離線鎖,因為悲觀離線鎖會在整個會話期間鎖住資料,所以會影響到系統的伸縮性和效能,選擇時要謹慎考慮幾方面的因素: 1 用例是否通過幾個資料庫事務來實現,第一個事務讀取資料,最後一個事務更新資料,首先要滿足這個條件我們才會選擇用悲觀離線鎖,否則如果在同一個資料庫事務,直接用離線鎖即可。 2 用例是否要求使用者提交的更新一定要成功,以及使用者提交事務後重新來一次的開銷大不大,如果要求使用者提交更新一定要成功,或者使用者提交更新失敗後,再重新來一次的開銷比較大,那麼就採取悲觀離線鎖,否則的話,我們可以採用樂觀離線鎖。註:詳細的悲觀離線鎖討論請參考馬丁大叔的POEAA.

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.