1. 程式人生 > 程式設計 >併發控制 - 樂觀/悲觀鎖

併發控制 - 樂觀/悲觀鎖

在網際網路高速發展的今天,網路流量所帶來的效益愈發明顯,但是高流量所帶來一個必然的聯絡就是高併發,而現代系統對於併發的處理有很多種方式,譬如多執行緒、非同步呼叫、核心功能加鎖、訊息佇列等,這篇文章主要就談論一下處理高併發的兩種思路,樂觀鎖(Optimistic Locking)和悲觀鎖(Pessimistic Concurrency Control)

併發問題

為了應對併發,開發者提出了事務的概念,以完成原子性的操作。但是在事務進行的過程中,同樣也會產生很多問題,譬如髒讀,不可重複讀,幻讀等,當然也就有不同的事務隔離級別去解決對應的問題。

髒讀

髒讀,指執行緒A在通過事務修改物件O的狀態但未提交時,執行緒B獲取到了物件O未被修改時的狀態,這時候執行緒B讀取到的資料就是髒資料,而根據髒資料所進行的操作,無法保證其的正確性。

舉一個簡單的栗子: 使用者A在某電商平臺下單了一件商品,根據後臺的業務邏輯,對應工作者執行緒將會開啟一個事務,扣除所對應的庫存數量,但同一時間,使用者B也下單可同樣的物品,在使用者A執行緒提交庫存扣除事務之前獲取到了庫存數量,之後執行了下單操作,可想而知使用者B所對應的工作者執行緒執行的一系列操作都是不正確的。

不可重複讀

不可重複讀,指執行緒A在同一個事務中,兩次完全相同的資料讀取操作,獲取到了不一樣的資料,原因可能是線上程A執行事務的過程中,執行緒B對統一資料提交了事務,導致兩次獲取資料不一樣。

幻讀

幻讀,和不可重複讀類似但其中又有所區別,同一個事務內多次查詢返回的結果集不一樣(比如增加了或者減少了行記錄)。

悲觀鎖

悲觀鎖,又叫悲觀併發控制(PCC),該鎖類似Java的顯式加鎖,之所以叫悲觀鎖,是因為對資料修改持有悲觀態度的併發控制,一般認為資料被併發修改的機率比較大,需要加鎖才能保證修改時的資料一致性。

悲觀鎖例項: 執行緒A、B併發修改資料O -> 執行緒A獲取到鎖,進行修改資料狀態操作 -> 執行緒A釋放鎖,執行緒B獲得鎖,進行資料狀態修改操作 -> 執行緒B釋放鎖 -> 修改操作完成

由於對資料進行了顯式加鎖,除了會產生額外的開銷以外,還會降低處理效率,增加死鎖機率。所以在目前資訊系統種,很少會再使用悲觀鎖。

悲觀鎖實現

悲觀鎖大多的實現方式都是基於資料庫的,即:

  • 在執行操作之前先給資料加排他鎖,若加鎖失敗視業務要求進行等待或者丟擲異常
  • 加鎖成功後執行資料更新修改操作,事務提交後會自動釋放鎖
// 開啟事務
BEGIN;
// FOR UPDATE 加鎖
SELECT amount FROM goods WHERE id = 233 FOR UPDATE;
// 進行資料操作
UPDATE goods SET amount = 15 WHERE id = 233;
// 提交釋放鎖
COMMIT;
複製程式碼

通過FOR UPDATE建立一個排他鎖,在事務提交前,無法對操作資料進行操作以保證資料的一致性。

TipMysql在Sql有用到索引的表資料時,會使用行級鎖,其它時間會用表級鎖。

樂觀鎖

樂觀鎖的樂觀,是相對於悲觀鎖的悲觀而存在的。樂觀鎖假設資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則讓返回使用者錯誤的資訊,讓使用者決定如何去做。

樂觀鎖例項: 執行緒A、執行緒B併發讀取資料並進行操作 -> 執行緒A完成操作,進行資料版本檢測,若無衝突,則提交事務 -> 執行緒B完成操作,進行版本檢測,檢測有衝突,不提交併返回異常

樂觀併發控制相信事務之間的資料競爭(data race)的概率是比較小的,因此儘可能直接做下去,直到提交的時候才去鎖定

樂觀鎖實現

樂觀鎖的主要實現方式為衝突檢測,CAS(Compare and swap),CAS是項樂觀鎖技術,當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。

簡單樂觀鎖實現:

// 得知amount = 4
SELECT amount FROM goods WHERE id = 233;
UPDATE goods SET amount = 2 WHERE id = 233 AND amount = 4;
複製程式碼

這樣就完成了不加鎖的資料更新,若資料一致,則更新,資料不一致,則為過期資料,可以重新發起請求。但這樣解決會引發ABA問題。

ABA問題

ABA問題指執行緒A讀取到資料amount數值為4,開始進行相關操作,執行緒B在同時修改了amount為2,又將amount修改為4,此時A執行緒操作執行完成,判斷amount是否等於4,等於則更新。這時候就產生了ABA問題,雖然執行緒A完成了資料更新,但不能保證過程的正確性。

解決ABA問題

為了確認資料版本和第一次獲取時一致,可以增加Version欄位,在更新資料時同步更新Version欄位狀態,這樣可以保證能知曉資料版本是否一致的問題。

// amount = 4,version = 1
SELECT amount,version FROM goods WHERE id = 233;
UPDATE goods SET amount = 3,version = 2 WHERE id = 233 AND version = 1;
複製程式碼

雖然增加version的操作可以保證資料版本的一致,但是這樣就會造成大量的資料修改失敗,這樣同樣會降低大量的處理效率。

故而產生了以下解決方案:

// amount = 4
SELECT amount FROM goods WHERE id = 233;
UPDATE goods SET amount = amount - 1 WHERE id = 233 AND amount > 0;
複製程式碼

即解決了Version欄位表入侵,又解決了大量修改失敗的問題。

總結

樂觀鎖其實並不是真正的鎖,只是一種併發控制思路,所以效率相較於普通的鎖來說更高,但是限制粒度不掌握好,就會導致大量的業務失敗。 而悲觀鎖雖然能保證資料的一致性,但是效率過低,不建議使用。

ps:個人部落格地址是shawJie.me,不定期會發布一些自己所經歷的,所學習的,所瞭解的,歡迎來坐坐。