Java開發技巧——併發控制中的樂觀鎖與悲觀鎖

千鋒武漢發表於2021-08-24

   1、為什麼需要鎖?

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

   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 ),並從其帳戶餘額中扣除 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 的操作結果的可能。

   8、總結

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

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

  本文來自千鋒教育,轉載請註明出處。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31548651/viewspace-2788426/,如需轉載,請註明出處,否則將追究法律責任。

相關文章