併發控制中的樂觀鎖與悲觀鎖

爲什麼需要鎖?

在多用戶環境中,在同一時間可能會有多個用戶更新相同的記錄,這會產生衝突。這就是著名的併發性問題。

2典型的衝突類型:

(1)丟失更新:一個事務的更新覆蓋了其它事務的更新結果,就是所謂的更新丟失。例如:用戶A把值從6改爲2,用戶B把值從2改爲6,則用戶A丟失了他的更新。

(2)髒讀:當一個事務讀取其它完成一半事務的記錄時,就會發生髒讀取。例如:用戶A,B看到的值都是6,用戶B把值改爲2,用戶A讀到的值仍爲6。

3併發控制的機制

悲觀鎖:假定會發生併發衝突,屏蔽一切可能違反數據完整性的操作。

樂觀鎖:假設不會發生併發衝突,只在提交操作時檢查是否違反數據完整性。 樂觀鎖不能解決髒讀的問題。

4樂觀鎖和悲觀鎖

悲觀鎖(Pessimistic Lock),就是很悲觀,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫如果提供類似於write_condition機制的其實都是提供的樂觀鎖。

兩種鎖各有優缺點:
不可認爲一種好於另一種,像樂觀鎖適用於寫比較少的情況下,即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常產生衝突,上層應用會不斷的進行retry,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適。

5樂觀鎖的應用

1、使用自增長的整數表示數據版本號。更新時檢查版本號是否一致,比如數據庫中數據版本爲6,更新提交時version=6+1,使用該version值(=7)與數據庫version+1(=7)作比較,如果相等,則可以更新,如果不等則有可能其他程序已更新該記錄,所以返回錯誤。

2、使用時間戳來實現. 

注:對於以上兩種方式,Hibernate自帶實現方式:在使用樂觀鎖的字段前加annotation: @Version, Hibernate在更新時自動校驗該字段。

6悲觀鎖的應用

需要使用數據庫的鎖機制,比如SQL SERVER 的TABLOCKX(排它表鎖) 此選項被選中時,SQL Server 將在整個表上置排它鎖直至該命令或事務結束。這將防止其他進程讀取或修改表中的數據。


在實際生產環境裏邊,如果併發量不大且不允許髒讀,可以使用悲觀鎖解決併發問題;但如果系統的併發非常大的話,悲觀鎖定會帶來非常大的性能問題,所以我們就要選擇樂觀鎖定的方法. 

悲觀鎖會造成訪問數據庫時間較長,併發性不好,特別是長事務。 
樂觀鎖在現實中使用得較多,廠商較多采用。

一個典型的倚賴數據庫的悲觀鎖調用:

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


這條 sql 語句鎖定了account 表中所有符合檢索條件( name=”Erica” )的記錄。本次事務提交之前(事務提交時會釋放事務過程中的鎖),外界無法修改這些記錄。

Hibernate 的悲觀鎖,也是基於數據庫的鎖機制實現。

注意,只有在查詢開始之前(也就是Hiberate 生成 SQL 之前)設定加鎖,纔會真正通過數據庫的鎖機制進行加鎖處理,否則,數據已經通過不包含 for update子句的 Select SQL 加載進來,所謂數據庫加鎖也就無從談起。

樂觀鎖( Optimistic Locking )相對悲觀鎖而言,樂觀鎖機制採取了更加寬鬆的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。

7經典案例分析

如一個金融系統,當某個操作員讀取用戶的數據,並在讀出的用戶數據的基礎上進行修改時(如更改用戶帳戶餘額),如果採用悲觀鎖機制,也就意味着整個操作過程中(從操作員讀出數據、開始修改直至提交修改結果的全過程,甚至還包括操作員中途去煮咖啡的時間),數據庫記錄始終處於加鎖狀態,可以想見,如果面對幾百上千個併發,這樣的情況將導致怎樣的後果。

樂觀鎖機制在一定程度上解決了這個問題。

樂觀鎖,大多是基於數據版本( Version )記錄機制實現。何謂數據版本?即爲數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過爲數據庫表增加一個 “version” 字段來實現。

讀取出數據時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認爲是過期數據。

對於上面修改用戶帳戶信息的例子而言,假設數據庫中帳戶信息表中有一個version 字段,當前值爲 1 ; 
而當前帳戶餘額字段( balance )爲 $100 。

1、操作員 A 此時將其讀出( version=1 ),並從其帳戶餘額中扣除 50100-$50 )。

2、在操作員 A 操作的過程中,操作員 B 也讀入此用戶信息( version=1 ),並從其帳戶餘額中扣除 20100-$20 )。

3、操作員 A 完成了修改工作,將數據版本號加一( version=2 ),連同帳戶扣除後餘額( balance=$50 ),提交至數據庫更新,此時由於提交數據版本大於數據庫記錄當前版本,數據被更新,數據庫記錄 version 更新爲 2 。

4、操作員 B 完成了操作,也將版本號加一( version=2 )試圖向數據庫提交數據( balance=$80 ),但此時比對數據庫記錄版本時發現,操作員 B 提交的數據版本號爲 2 ,數據庫記錄當前版本也爲 2 ,不滿足 “ 提交版本必須大於記錄當前版本才能執行更新 “ 的樂觀鎖策略,因此,操作員 B 的提交被駁回。這樣,就避免了操作員 B 用基於 version=1 的舊數據修改的結果覆蓋操作員 A 的操作結果的可能。

8總結

從上面的例子可以看出,樂觀鎖機制避免了長事務中的數據庫加鎖開銷(操作員 A和操作員 B 操作過程中,都沒有對數據庫數據加鎖),大大提升了大併發量下的系統整體性能表現。

需要注意的是,樂觀鎖機制往往基於系統中的數據存儲邏輯,因此也具備一定的侷限性,如在上例中,由於樂觀鎖機制是在我們的系統中實現,來自外部系統的用戶餘額更新操作不受我們系統的控制,因此可能會造成髒數據被更新到數據庫中。在系統設計階段,我們應該充分考慮到這些情況出現的可能性,並進行相應調整(如將樂觀鎖策略在數據庫存儲過程中實現,對外只開放基於此存儲過程的數據更新途徑,而不是將數據庫表直接對外公開)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章