併發控制 - 樂觀/悲觀鎖
在網際網路高速發展的今天,網路流量所帶來的效益愈發明顯,但是高流量所帶來一個必然的聯絡就是高併發,而現代系統對於併發的處理有很多種方式,譬如多執行緒、非同步呼叫、核心功能加鎖、訊息佇列等,這篇文章主要就談論一下處理高併發的兩種思路,樂觀鎖(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,不定期會發布一些自己所經歷的,所學習的,所瞭解的,歡迎來坐坐。