行鎖

i路人甲i發表於2020-12-29

MySQL 的行鎖是在引擎層由各個引擎自己實現的。但並不是所有的引擎都支援行鎖。比如 MyISAM 引擎就不支援行鎖。InnoDB 是支援行鎖的,這也是 MyISAM 被 InnoDB 替代的重要原因之一。

兩階段鎖

在 InnoDB 事務中,行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。如果你的事務中需要鎖多個行,要把最可能造成鎖衝突、最可能影響併發度的鎖的申請時機儘量往後放。

死鎖和死鎖檢測

當併發系統中不同執行緒出現迴圈資源依賴,涉及的執行緒都在等待別的執行緒釋放資源時,就會導致這幾個執行緒都進入無限等待的狀態,稱為死鎖。

事務A
事務B
begin;update test set k=k+1 where id=1;begin;
update test set k=k+1 where id=2;
update test set k=k+1 where id=2;
update test set k=k+1 where id=1;

這個時候 A 在等待事務 B 釋放 id=2 的行鎖,而事務 B 在等待事務 A 釋放 id=1 的行鎖。事務 A 和事務 B 在互相等待對方釋放資源形成死鎖。當出現死鎖後有兩種策略:

  • 直接進入等待,直到超時。這個超時時間可以通過引數 innodb_lock_wait_timeout 來設定。
  • 發起死鎖檢測,發現死鎖後主動回滾死鎖鏈條中的某一事務,讓其他事務繼續執行。將引數 innodb_deadlock_detect 設定為 on 表示開啟這個邏輯。

在 InnoDB 中,innodb_lock_wait_timeout 的預設值是 50s,這就表明如果出現死鎖需要等待 50s 第一個被鎖住的執行緒才會退出,這對業務是無法接受。如果我們把這個值設為很小的值,比如 1s ,如果只是簡單的鎖等待也會被“誤傷”。

正常情況下我們採用第二種策略,即:主動死鎖檢測,而且 innodb_deadlock_detect 的預設值本身就是 on。主動死鎖檢測在發生死鎖的時候,是能夠快速發現並進行處理的,但是它也是有額外負擔的。每當一個事務被鎖的時候,就要看看它所依賴的執行緒有沒有被別人鎖住,如此迴圈,最後判斷是否出現了迴圈等待,也就是死鎖。

如果所有事務都要更新同一行,每個新來的被堵住的執行緒,都要判斷會不會由於自己的加入導致了死鎖,這是一個時間複雜度是 O(n) 的操作。假設有 1000 個併發執行緒要同時更新同一行,那麼死鎖檢測操作就是 100 萬這個量級的。雖然最終檢測的結果是沒有死鎖,但是這期間要消耗大量的 CPU 資源。

解決由這種熱點行更新導致的效能問題主要方法:

  • 一種頭痛醫頭的方法,就是如果你能確保這個業務一定不會出現死鎖,可以臨時把死鎖檢測關掉。關掉死鎖檢測意味著可能會出現大量的超時,這是對業務有損的。
  • 是控制併發度。如果併發能夠控制住,比如同一行同時最多隻有 10 個執行緒在更新,那麼死鎖檢測的成本很低。這個併發控制要做在資料庫服務端。如果你有中介軟體,可以考慮在中介軟體實現;如果你的團隊有能修改 MySQL 原始碼的人,也可以做在 MySQL 裡面。基本思路就是,對於相同行的更新,在進入引擎之前排隊。如果做在客戶端,當客戶端數量過多時,也會造成效能問題。
  • 通過將一行改成邏輯上的多行來減少鎖衝突。比如更新一條賬戶的金額,可以把金額拆成多條,每次更新隨機選擇一條。

相關文章