目錄
1、樂觀鎖介紹
2、示例
3、優點
4、缺點
5、實現
1、樂觀鎖介紹
樂觀鎖(Optimistic Locking)相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。而樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於數據版本(Version)記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現。讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認為是過期數據。
2、示例
如一個金融系統,當某個操作員讀取用戶的數據,並在讀出的用戶數據的基礎上進行修改時(如更改用戶帳戶余額),如果采用悲觀鎖機制,也就意味着整個操作過程中(從操作員讀出數據、開始修改直至提交修改結果的全過程,甚至還包括操作員中途去煮咖啡的時間),數據庫記錄始終處於加鎖狀態,可以想見,如果面對幾百上千個並發,這樣的情況將導致怎樣的后果。
樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於數據版本(Version)記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現。
讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認為是過期數據。
對於上面修改用戶帳戶信息的例子而言,假設數據庫中帳戶信息表中有一個version字段,當前值為 1 ;而當前帳戶余額字段( balance )為 $100 。
a、操作員 A 此時將其讀出( version=1 ),並從其帳戶余額中扣除 $50( $100-$50 )。
b、在操作員 A 操作的過程中,操作員B 也讀入此用戶信息( version=1 ),並從其帳戶余額中扣除 $20 ( $100-$20 )。
c、操作員 A 完成了修改工作,將數據版本號加一( version=2 ),連同帳戶扣除后余額( balance=$50 ),提交至數據庫更新,此時由於提交數據版本大於數據庫記錄當前版本,數據被更新,數據庫記錄 version 更新為 2 。
d、操作員 B 完成了操作,也將版本號加一( version=2 )試圖向數據庫提交數據( balance=$80 ),但此時比對數據庫記錄版本時發現,操作員 B 提交的數據版本號為 2 ,數據庫記錄當前版本也為 2 ,不滿足 “ 提交版本必須大於記錄當前版本才能執行更新 “ 的樂觀鎖策略,因此,操作員 B 的提交被駁回。
這樣,就避免了操作員 B 用基於 version=1 的舊數據修改的結果覆蓋操作員A 的操作結果的可能。
3、優點
從上面的例子可以看出,樂觀鎖機制避免了長事務中的數據庫加鎖開銷(操作員A和操作員B操作過程中,都沒有對數據庫數據加鎖),大大提升了大並發量下的系統整體性能表現。
4、缺點
需要注意的是,樂觀鎖機制往往基於系統中的數據存儲邏輯,因此也具備一定的局限性,如在上例中,由於樂觀鎖機制是在我們的系統中實現,來自外部系統的用戶余額更新操作不受我們系統的控制,因此可能會造成臟數據被更新到數據庫中。在系統設計階段,我們應該充分考慮到這些情況出現的可能性,並進行相應調整(如將樂觀鎖策略在數據庫存儲過程中實現,對外只開放基於此存儲過程的數據更新途徑,而不是將數據庫表直接對外公開)。
5、實現
Hibernate支持樂觀鎖,保存數據時Hibernate會自動完成檢查Version列、修改數據、更新Version列等工作。Hibernate隱藏了所有的Version操作細節,開發者只須指定實體類的Version列即可。實體類中可用@Version配置版本屬性。版本列一般為數字類型屬性。例如:
@Entity @Table(name="user") public class User{ @Id @GeneratedValue private int id; @Column(name="name", length = 100) private String name; @Version private int version; public User(){} //getter... //setter... }
注意version節點必須出現在ID節點之后。
這里我們聲明了一個version屬性,用於存放用戶的版本信息,保存在User表的version字段中。
此時如果我們嘗試編寫一段代碼,更新User表中記錄數據,如:
Criteria criteria = session.createCriteria(User.class); criteria.add(Expression.eq("name","Erica")); List userList = criteria.list(); User user =(User)userList.get(0); Transaction tx = session.beginTransaction(); user.seUserType(1); //更新UserType字段 tx.commit();
每次對User進行更新的時候,我們可以發現,數據庫中的version都在遞增。而如果我們嘗試在tx.commit之前,啟動另外一個Session,對名為Erica的用戶進行操作,以模擬並發更新時的情形:
Session session= getSession(); //A Criteria criteria = session.createCriteria(User.class); criteria.add(Expression.eq("name","Erica")); Session session2 = getSession(); //B Criteria criteria2 = session2.createCriteria(User.class); criteria2.add(Expression.eq("name","Erica")); List userList = criteria.list(); List userList2 = criteria2.list(); User user =(User)userList.get(0); User user2 =(User)userList2.get(0); Transaction tx = session.beginTransaction(); //A Transaction tx2 = session2.beginTransaction(); //B user2.seUserType(99); tx2.commit(); //B user.seUserType(1); tx.commit(); //A
執行以上代碼,代碼將在tx.commit()處拋出StaleObjectStateException異常,並指出版本檢查失敗,當前事務正在試圖提交一個過期數據。通過捕捉這個異常,我們就可以在樂觀鎖校驗失敗時進行相應處理。
不管是悲觀鎖定還是樂觀鎖定都可以利用select for update nowaut查詢來驗證行未被修改。悲觀鎖定會在用戶有意修改數據那一刻使用這條語句。樂觀鎖定則在即將在數據庫中更新數據時使用這條語句。這樣不僅能解決應用中的阻塞問題,還可以修正數據完整性問題。
如果,您認為閱讀這篇博客讓您有些收獲,不妨點擊一下右下角的【推薦】。
如果,您希望更容易地發現我的新博客,不妨點擊一下左下角的【關注我】。
如果,您對我的博客所講述的內容有興趣,請繼續關注我的后續博客,我是【Ruthless】。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。