Reference:https://time.geekbang.org/column/article/117247
死鎖產生
行鎖的具體實現算法有三種:record lock、gap lock以及next-key lock。
- record lock是專門對索引項加鎖;
- gap lock是對索引項之間的間隙加鎖;
- next-key lock則是前面兩種的組合,對索引項及其之間的間隙加鎖。
只在可重復讀或以上隔離級別下的特定操作才會取得gap lock或next-key lock,在Select、Update和Delete時,除了基於唯一索引的查詢之外,其它索引查詢時都會獲取gap lock或next-key lock,即鎖住其掃描的范圍。主鍵索引也屬於唯一索引,所以主鍵索引是不會使用gap lock或next-key lock。
在MySQL中,gap lock默認是開啟的,即innodb_locks_unsafe_for_binlog參數值是disable的,且MySQL中默認的是RR事務隔離級別。
當執行以下查詢SQL時,由於order_no列為非唯一索引,此時又是RR事務隔離級別,所以SELECT的加鎖類型為gap lock,這里的gap范圍是(4,+∞)。
1 SELECT id FROM demo.order_record where order_no = 4 for update;
執行查詢SQL語句獲取的gap lock並不會導致阻塞,而當執行以下插入SQL時,會在插入間隙上再次獲取插入意向鎖。
插入意向鎖其實也是一種gap鎖,它與gap lock是沖突的,所以當其它事務持有該間隙的gap lock時,需要等待其它事務釋放gap lock之后,才能獲取到插入意向鎖。
以上事務A和事務B都持有間隙(4,+∞)的gap鎖,而接下來的插入操作為了獲取到插入意向鎖,都在等待對方事務的gap鎖釋放,於是就造成了循環等待,導致死鎖。
1 INSERT INTO demo.order_record(order_no, status, create_date) VALUES (5, 1, '2019-08-30 12:22:22');
可以通過以下鎖的兼容矩陣圖,來查看鎖的兼容性:
避免死鎖的措施
避免死鎖最直觀的方法就是在兩個事務相互等待時,當一個事務的等待時間超過設置的某一閾值,就對這個事務進行回滾,另一個事務就可以繼續執行了。這種方法簡單有效,在InnoDB中,參數innodb_lock_wait_timeout是用來設置超時時間的。
另外,還可以將order_no列設置為唯一索引列。雖然不能防止幻讀,但可以利用它的唯一性來保證訂單記錄不重復創建,這種方式唯一的缺點就是當遇到重復創建訂單時會拋出異常。
還可以使用其它的方式來代替數據庫實現冪等性校驗。例如,使用Redis以及ZooKeeper來實現,運行效率比數據庫更佳。
常見死鎖的問題
死鎖的四個必要條件:互斥、占有且等待、不可強占用、循環等待。只要系統發生死鎖,這些條件必然成立。
InnoDB存儲引擎的主鍵索引為聚簇索引,其它索引為輔助索引。
如果之前使用輔助索引來更新數據庫,就需要修改為使用聚簇索引來更新數據庫。
如果兩個更新事務使用了不同的輔助索引,或一個使用了輔助索引,一個使用了聚簇索引,就都有可能導致鎖資源的循環等待。由於本身兩個事務是互斥,也就構成了以上死鎖的四個必要條件了。
綜上可知,在更新操作時,應該盡量使用主鍵來更新表字段,這樣可以有效避免一些不必要的死鎖發生。
解決死鎖的最佳方式就是預防死鎖:
- 在編程中盡量按照固定的順序來處理數據庫記錄,假設有兩個更新操作,分別更新兩條相同的記錄,但更新順序不一樣,有可能導致死鎖;
- 在允許幻讀和不可重復讀的情況下,盡量使用RC事務隔離級別,可以避免gap lock導致的死鎖問題;
- 更新表時,盡量使用主鍵更新;
- 避免長事務,盡量將長事務拆解,可以降低與其它事務發生沖突的概率;
- 設置鎖等待超時參數,可以通過innodb_lock_wait_timeout設置合理的等待超時閾值,特別是在一些高並發的業務中,可以盡量將該值設置得小一些,避免大量事務等待,占用系統資源,造成嚴重的性能開銷。