J2EE事務併發控制策略總結

火星人 @ 2014-03-10 , reply:0


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

事務併發訪問控制策略

當前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語句來載入對象,從而鎖定對應的記錄,避免其它事務併發更新.

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

C 與java是兩種完全不同風格的東西,C 是由程序員創造的,由程序員完善的,然後才出的標準的,也就是說C 的標準完全落後與C 的發展.java恰好相反,它是先有標準(可能還沒有實現),然後後有的實現,它是由公司主導開發的,雖然現在開源了,但是標準並不是誰都能定的.這就造就了C 是百花齊放,博大精深,很少有人敢說自己C 很厲害.java卻是另外的一種感覺,一切都規定好了,你只需要按照規定去做,符合標準才可以的.C 是那種既可以做的堂堂正正,博大精深(比如標準庫),又可以實現的匪夷所思,天馬行空(寫 Boost庫的人太牛了).java不行,java要求如此只能如此,不能越雷池一步.




[火星人 via ] J2EE事務併發控制策略總結已經有101次圍觀

http://www.coctec.com/docs/java/show-post-62214.html