死鎖是資料庫並發控制中無法徹底消滅的現象,但只要掌握其觸發條件就能大幅降低發生頻率。最常見的四大死鎖根源是:多事務鎖請求順序不一致、事務執行時間過長導致的鎖持有時間延長、索引設計不當造成鎖範圍過擴張、以及隔離等級設為Serializable時鎖粒度的劇烈放大。規避死鎖的核心策略是將所有事務以統一的順序存取資源,並進一步縮短鎖的有效持有跨度,而檢察死鎖時則要優先檢查系統日誌中的鎖序列圖和等鎖鏈路的鎖鏈路。
據資料庫領域研究者、資訊研究所教授Jeffrey Ullman在《資料庫系統概論》中的歸納:系統性死鎖必須同時成立互斥條件、持有條件、循環條件與不剝奪條件,才可能形成死鎖。四者中任何一個條件被破壞,死鎖就會瓦解。
以MySQL的InnoDB為例,兩個事務同時各持有一把鎖,而彼此又要求對方持有的資源,循環等待即告成立,若系統不干預,兩邊都無法進入下一段執行,形成死鎖等待。所以檢查死鎖的第一步,永遠是確認是否四條件同時成立,而非盲目調整參數。
多個事務沿用不同順序訪問相同資料表或行時,發生「互為等待」的機率會急劇升高。事務A先更新表格task再更新表格user,事務B卻先更新user再更新task,兩個事務在同一時間點反向請求鎖,就可能發生循環等待。
例如,若所有事務都先鎖user再鎖task,即使在一定時間內排隊較長,也不會產生循環型死鎖。鎖順序統一之後,就從根上破壞了循環條件。
執行越久、撬鎖越久,其他事務等待該資源的時間就越長,產生死鎖的機率也隨之放大。據DB-Engines關於MySQL領域的熱門案例觀察:在典型的OLTP業務中,事務平均執行時間超過300ms的系統,發作死鎖事件的頻率明顯高於100ms內完成的系統。
SELECT * FROM user WHERE id=10 FOR UPDATE UPDATE task SET status=2 WHERE task_id=100; COMMIT;
上述語句若在業務代碼中停留超過秒級,再加上同頁其他事務都恰在等待這一行,就會阻塞極高。短事务即紧縮持鎖時間可以降低锁的重叠窗口,同時改變每次鎖管理的資料量,讓阻塞的機率下降数十倍。
InnoDB在WHERE查詢中若無法利用索引過濾精確行,通常会发生鎖定整個區間的所有記錄而非只鎖需要的行(即鎖膨脹)。這意味着事務對非索引欄位進比查時會影響一組行,導致其他事務因請求同一範圍內的記錄而互相阻塞。
例如 update task set status=1 where user_id>0 會鎖定user_id對應的所有行的間隙,若同資料表還有其他事務可能在更新另一批的行,就會間接造成嚴重的鎖佇列上。很多線上死鎖案例都能追蹤回索引缺失這一層面。
將隔離等級定為REPEATABLE READ或SERIALIZABLE時,間隙鎖的使用會異常頻繁,因此在偏重報表或廣範圍查詢的環境,SERIALIZABLE會擴大鎖冲突面積並引來死鎖。
| 隔離等級 | 鎖使用特徵 | 死鎖傾向 |
|---|---|---|
| READ COMMITTED | 無間隙鎖,僅行鎖 | 低,但需配掉其他一致性 |
| REPEATABLE READ | 含間隙鎖(Gap Lock) | 中,需要防化可重複讀帶入的範圍競爭 |
| SERIALIZABLE | 鎖幾乎覆蓋全範圍 | 高,尤其處理範圍查詢時風險巨大 |
在實際開,將事務已合理退步到READ COMMITTED並組合恰當的事務序列能顯著降低死鎖襲擊。如果因業務邏輯必須維持更高級別,只能在檢討這類高鎖隔離環境的執行路徑,重點清掉長時間的範圍查詢和可能觸發相斥鎖的SQL。
鎖的哪個環節占用,經常可用資料庫自身的約定視圖或系統變數來判斷。以下是三板斧:MySQL查行死鎖:使用SHOW ENGINE INNODB STATUS中的LATEST DETECTED DEADLOCK區塊,輸出中可直接看到兩個事務所執行的SQL與互相持有的鎖。在Oracle則查與事務鎖相關的視圖如v$locked_object。SQL Server可以明查sys.dm_tran_locks。
SELECT table_name, transaction_id, lock_type, lock_mode, duration
FROM performance_schema.innodb_trx JOIN
performance_schema.innodb_locks USING (lock_id)
WHERE lock_transaction_id IS NOT NULL;
查看結果後,可以找到持有時間最長的鎖,審查其對應SQL的事務範圍是否過大。若要急診處理死鎖,可以選擇在等鎖超時參數上限制,如InnoDB通過設定innodb_lock_wait_timeout,讓系統在極端情況下自動放棄等待而非無限阻塞——但這不會消除死鎖,只是為等待設限。
大部分資料庫都內建死鎖檢測機制,例如MySQL會偵測並回滾受害事務的其中一個,令其徹底回滾,解除循環鎖。但自動檢測機制只在死鎖發生後有所作為,完全不能防堵其再次出現,因此業務代碼中無論如何要設計重試機制來對抗偶發的死鎖回滾。
應用層可以對死鎖錯誤(如MySQL錯誤碼1213)進行單獨記錄和有序重試,注意單側進行重試的鎖重複被回滾不能無限迴圈,需要設定重試總次數,並在前後執行順序上嚴控。將各事務按照既定的資源順序去操作,是相遇於其他程度的更根本保險。
順序不統一、事务長、索引缺、鎖脾氣過烈——串列鏈外部都容易同時夾帶出死鎖。資料庫本身不會「自癒」這些高風險結構面的問題,唯有把事務資源順序梳理統一、SQL中的索引實際使用SQL實際下的鎖範圍,再配超時與充的補救機制,才能把死鎖的頻率壓到業務可接受的程度。平時做好持續排查all鎖的警訊,從上面的四個層面逐一檢查,才是最可靠的控制鬆緊方法。
评论
加载中...