Hibernate 中悲觀鎖和樂觀鎖

Hibernate 中悲觀鎖和樂觀鎖

鎖(locking)
業務邏輯的實現過程中,往往需要保證數據訪問的排他性。如在金融系統的日終結算處理中,我們希望針對某個cut-off時間點的數據進行處理,而不希望在結算進行過程中 (可能是幾秒種,也可能是幾個小時),數據再發生變化。此時,我們就需要通過一些機制來保證這些數據在某個操作過程中不會被外界修改,這樣的機制,在這裏,也就是所謂 的“鎖”,即給我們選定的目標數據上鎖,使其無法被其他程序修改。 Hibernate支持兩種鎖機制:即通常所說的“悲觀鎖(Pessimistic Locking)” 和“樂觀鎖(Optimistic Locking)”。

一 :悲觀鎖(Pessimistic Locking)
悲觀鎖,正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處於鎖定 狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能 真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系 統不會修改數據)。 一個典型的倚賴數據庫的悲觀鎖調用: select * from account where name=”Erica” for update 這條sql 語句鎖定了account 表中所有符合檢索條件(name=”Erica”)的記錄。 本次事務提交之前(事務提交時會釋放事務過程中的鎖),外界無法修改這些記錄。 Hibernate的悲觀鎖,也是基於數據庫的鎖機制實現。 下面的代碼實現了對查詢記錄的加鎖:
1 String hqlStr = " from TUser as user where user.name=’Erica’ " ;
2 Query query = session.createQuery(hqlStr);
3 query.setLockMode( " user " ,LockMode.UPGRADE); // 加鎖
4 List userList = query.list(); // 執行查詢,

獲取數據 query.setLockMode 對查詢語句中特定別名所對應的記錄進行加鎖(我們爲 TUser類指定了一個別名“user”),這裏也就是對返回的所有user記錄進行加鎖。 觀察運行期Hibernate生成的SQL語句:
1 select tuser0_.id as id, tuser0_.name as name, tuser0_.group_id as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex from t_user tuser0_ where (tuser0_.name = ’Erica’ ) for update

這裏Hibernate通過使用數據庫的for update子句實現了悲觀鎖機制。 Hibernate的加鎖模式有:
1 LockMode.NONE : 無鎖機制。
2 LockMode.WRITE :Hibernate在Insert和Update記錄的時候會自動 獲取。
3 LockMode.READ : Hibernate在讀取記錄的時候會自動獲取。

以上這三種鎖機制一般由Hibernate內部使用,如Hibernate爲了保證Update 過程中對象不會被外界修改,會在save方法實現中自動爲目標對象加上WRITE鎖。

1 LockMode.UPGRADE :利用數據庫的for update子句加鎖。
2LockMode. UPGRADE_NOWAIT :Oracle的特定實現,利用Oracle的for update nowait子句實現加鎖。

上面這兩種鎖機制是我們在應用層較爲常用的,加鎖一般通過以下方法實現:
Criteria.setLockMode
Query.setLockMode
Session.lock
注意,只有在查詢開始之前(也就是Hiberate 生成SQL 之前)設定加鎖,纔會 真正通過數據庫的鎖機制進行加鎖處理,否則,數據已經通過不包含for update 子句的Select SQL加載進來,所謂數據庫加鎖也就無從談起。

**********************************************

基於jdbc實現的數據庫加鎖如下:

select * from account where name="Erica" for update


在更新的過程中,數據庫處於加鎖狀態,任何其他的針對本條數據的操作都將被延遲。本次事務提交後解鎖。
而hibernate悲觀鎖的具體實現如下:
String sql="查詢語句";
Query query=session.createQuery(sql);
query.setLockMode("對象",LockModel.UPGRADE);

說到這裏,就提到了hibernate的加鎖模式:

LockMode.NONE:無鎖機制。
LockMode.WRITE:Hibernate在Insert和Update記錄的時候會自動獲取。
LockMode.READ:Hibernate在讀取記錄的時候會自動獲取。

這三種加鎖模式是供hibernate內部使用的,與數據庫加鎖無關:

LockMode.UPGRADE:利用數據庫的for update字句加鎖。

在這裏我們要注意的是:只有在查詢開始之前(也就是hiernate生成sql語句之前)加鎖,纔會真正通過數據庫的鎖機制加鎖處理。否則,數據已經通過不包含for updata子句的sql語句加載進來,所謂的數據庫加鎖也就無從談起。
但是,從系統的性能上來考慮,對於單機或小系統而言,這並不成問題,然而如果是在網絡上的系統,同時間會有許多聯機,假設有數以百計或上千甚至更多的併發訪問出現,我們該怎麼辦?如果等到數據庫解鎖我們再進行下面的操作,我們浪費的資源是多少?--這也就導致了樂觀鎖的產生。

二 :樂觀鎖(Optimistic Locking)
相對悲觀鎖而言,樂觀鎖機制採取了更加寬鬆的加鎖機制。悲觀鎖大多數情況下依 靠數據庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是數據庫 性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。 如一個金融系統,當某個操作員讀取用戶的數據,並在讀出的用戶數據的基礎上進 行修改時(如更改用戶帳戶餘額),如果採用悲觀鎖機制,也就意味着整個操作過 程中(從操作員讀出數據、開始修改直至提交修改結果的全過程,甚至還包括操作 員中途去煮咖啡的時間),數據庫記錄始終處於加鎖狀態,可以想見,如果面對幾 百上千個併發,這樣的情況將導致怎樣的後果。 樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖 大多是基於數據版本 (Version)記錄機制實現。何謂數據版本?即爲數據增加一個版本標識,在基於 數據庫表的版本解決方案中,一般是通過爲數據庫表增加一個“version”字段來 實現。 讀取出數據時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提 交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據 版本號大於數據庫表當前版本號,則予以更新,否則認爲是過期數據。 對於上面修改用戶帳戶信息的例子而言,假設 :

數據庫中帳戶信息表中有一個 version字段,當前值爲1;而當前帳戶餘額字段(balance)爲$100。
1 :操作員A 此時將其讀出(version=1),並從其帳戶餘額中扣除$50 ($100-$50)。
2 : 在操作員A操作的過程中,操作員B也讀入此用戶信息(version=1),並 從其帳戶餘額中扣除$20($100-$20)。
3: 操作員A完成了修改工作,將數據版本號加一(version=2),連同帳戶扣 除後餘額(balance=$50),提交至數據庫更新,此時由於提交數據版本大 於數據庫記錄當前版本,數據被更新,數據庫記錄version更新爲2。
4: 操作員B完成了操作,也將版本號加一(version=2)試圖向數據庫提交數 據(balance=$80),但此時比對數據庫記錄版本時發現,操作員B提交的 數據版本號爲2,數據庫記錄當前版本也爲2,不滿足“提交版本必須大於記 錄當前版本才能執行更新“的樂觀鎖策略,因此,操作員B 的提交被駁回。 這樣,就避免了操作員B 用基於version=1 的舊數據修改的結果覆蓋操作 員A的操作結果的可能。

從上面的例子可以看出,樂觀鎖機制避免了長事務中的數據庫加鎖開銷(操作員A 和操作員B操作過程中,都沒有對數據庫數據加鎖),大大提升了大併發量下的系 統整體性能表現。 需要注意的是,樂觀鎖機制往往基於系統中的數據存儲邏輯,因此也具備一定的局 限性,如在上例中,由於樂觀鎖機制是在我們的系統中實現,來自外部系統的用戶 餘額更新操作不受我們系統的控制,因此可能會造成髒數據被更新到數據庫中。在 系統設計階段,我們應該充分考慮到這些情況出現的可能性,並進行相應調整(如 將樂觀鎖策略在數據庫存儲過程中實現,對外只開放基於此存儲過程的數據更新途 徑,而不是將數據庫表直接對外公開)。 Hibernate 在其數據訪問引擎中內置了樂觀鎖實現。如果不用考慮外部系統對數 據庫的更新操作,利用Hibernate提供的透明化樂觀鎖實現,將大大提升我們的 生產力。 Hibernate中可以通過class描述符的optimistic-lock屬性結合version 描述符指定。
現在,我們爲之前示例中的TUser加上樂觀鎖機制。

1. 首先爲TUser的class描述符添加optimistic-lock屬性:
< hibernate - mapping >
< class
name = " org.hibernate.sample.TUser "
table = " t_user "
dynamic - update = " true "
dynamic - insert = " true "
optimistic - lock = " version "
>
……
</ class >
</ hibernate - mapping >

optimistic-lock屬性有如下可選取值:
1 none 無樂觀鎖
2 version 通過版本機制實現樂觀鎖
3 dirty 通過檢查發生變動過的屬性實現樂觀鎖
4 all 通過檢查所有屬性實現樂

下面就來在前面例子的基礎上進行Hibernate樂觀鎖的測試。 首先需要修改前面所實現的業務對象,在其中增加一個version屬性,用來記錄該對象所包含數據的版本信息,修改後的User對象如清單14.5所示。

清單14.5 修改後的User對象

package cn.hxex.hibernate.lock;

public class User {

private String id;

private Integer version; // 增加版本屬性

private String name;

private Integer age;

// 省略了getter和setter方法 ……

} 然後是修改映射文件,增加version屬性的配置。在這裏需要注意的是,這裏的version屬性應該使用專門的<version>元素來進行配置,這樣才能使其發揮樂觀鎖的作用。如果還使用<property>元素來進行配置,那麼Hibernate只會將其作爲一個普通的屬性來進行處理。

修改後的映射文件

<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC

"-//Hibernate/Hibernate Mapping DTD 3.0//EN"

"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="cn.hxex.hibernate.lock">

<class name="User" table="USERINFO" optimistic-lock="version">

<id name="id" column="userId">

<generator class="uuid.hex"/>

</id>

<version name="version" column="version" type="java.lang.Integer"/>

<property name="name" column="name" type="java.lang.String"/>

<property name="age" column="age" type="java.lang.Integer"/>

</class>

</hibernate-mapping>

*************************************************

樂觀鎖定(optimistic locking)則樂觀的認爲資料的存取很少發生同時存取的問題,因而不作數據庫層次上的鎖定,爲了維護正確的數據,樂觀鎖定採用應用程序上的邏輯實現版本控制的方法。

例如若有兩個客戶端,A客戶先讀取了賬戶餘額100元,之後B客戶也讀取了賬戶餘額100元的數據,A客戶提取了50元,對數據庫作了變更,此時數據庫中的餘額爲50元,B客戶也要提取30元,根據其所取得的資料,100-30將爲70餘額,若此時再對數據庫進行變更,最後的餘額就會不正確。

在不實行悲觀鎖定策略的情況下,數據不一致的情況一但發生,有幾個解決的方法,一種是先更新爲主,一種是後更新的爲主,比較複雜的就是檢查發生變動的數據來實現,或是檢查所有屬性來實現樂觀鎖定。

Hibernate 中透過版本號檢查來實現後更新爲主,這也是Hibernate所推薦的方式,在數據庫中加入一個VERSON欄記錄,在讀取數據時連同版本號一同讀取,並在更新數據時遞增版本號,然後比對版本號與數據庫中的版本號,如果大於數據庫中的版本號則予以更新,否則就回報錯誤。

以剛纔的例子,A客戶讀取賬戶餘額1000元,並連帶讀取版本號爲5的話,B客戶此時也讀取賬號餘額1000元,版本號也爲5,A客戶在領款後賬戶餘額爲 500,此時將版本號加1,版本號目前爲6,而數據庫中版本號爲5,所以予以更新,更新數據庫後,數據庫此時餘額爲500,版本號爲6,B客戶領款後要變更數據庫,其版本號爲5,但是數據庫的版本號爲6,此時不予更新,B客戶數據重新讀取數據庫中新的數據並重新進行業務流程才變更數據庫。

以Hibernate實現版本號控制鎖定的話,我們的對象中增加一個version屬性,例如:

public class Account {
private int version;
....
public void setVersion(int version) {
this.version = version;
}
public int getVersion() {
return version;
}
....
}

而在映像文件中,我們使用optimistic-lock屬性設定version控制,<id>屬性欄之後增加一個<version>標籤,如下:

<hibernate-mapping>
<class name="onlyfun.caterpillar.Account" talble="ACCOUNT"
optimistic-lock="version">
<id...../>
<version name="version" column="VERSION"/>
....
</class>
</hibernate-mapping>

設定好版本控制之後,在上例中如果B 客戶試圖更新數據,將會引發StableObjectStateException例外,我們可以捕捉這個例外,在處理中重新讀取數據庫中的數據,同時將 B客戶目前的數據與數據庫中的數據秀出來,讓B客戶有機會比對不一致的數據,以決定要變更的部份,或者您可以設計程式自動讀取新的資料,並重復扣款業務流程,直到數據可以更新爲止,這一切可以在背景執行,而不用讓您的客戶知道。

但是樂觀鎖也有不能解決的問題存在:上面已經提到過樂觀鎖機制的實現往往基於系統中的數據存儲邏輯,在我們的系統中實現,來自外部系統的用戶餘額更新不受我們系統的控制,有可能造成非法數據被更新至數據庫。因此我們在做電子商務的時候,一定要小心的注意這項存在的問題,採用比較合理的邏輯驗證,避免數據執行錯誤。

也可以在使用Session的load()或是lock()時指定鎖定模式以進行鎖定。

如果數據庫不支持所指定的鎖定模式,Hibernate會選擇一個合適的鎖定替換,而不是丟出一個例外。
發佈了55 篇原創文章 · 獲贊 17 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章