type
status
date
slug
summary
tags
category
icon
password
前言
曾經在正式環境遇過一個特殊的情境,我們都知道
DEADLOCK 的成因在於不同的加鎖順序,加上這兩句 WHERE條件是相似的語法,卻還是遇到了 DEADLOCK,因此我的第一個反應是兩個 TRANSACTION 中已執行語句的 LOCK 未釋放,導致互相阻塞對方引起的。不過事後查看 LOG 加上詢問開發確認了該語句皆是單獨執行,只好回頭透過 EXPLAIN 確認執行計畫,發現兩句語法走了不同的 INDEX以此找到思路,最後了解發生成因並找到解決方案,更重要的是知道了 隱式鎖(implicit lock)的存在。重現


單獨測試
- 查看兩句語法單獨執行的 LOCK 狀況


- 先執行 Transaction 1 再執行 Transaction 2

- 先執行 Transaction 2 再執行 Transaction 1,可以發現 Transaction 2 多了一個 LOCK

經由以上測試,我們確認了此例的 Lock 的成因如下表格-

在分析個過程中,我們可以看到當優先執行
Transaction 2時,我們可以看到最後一個 wating Lock ID_report (no gap) 是不存在的,直到我們執行 Transaction 1時,這一個 LOCK才會出現,這就是 隱式鎖(implicit lock) 。什麼是隱式鎖?
在 MySQL官方文檔 中關於隱式鎖就只有這麼一條說明-
When UPDATE modifies a clustered index record, implicit locks are taken on affected secondary index records.
資訊量實在不是很足夠,再加上討論到的文章其實不是很多,因此只好搭配少量的文章搭配源碼來了解,下方出現的源碼取自 8.0 版本,並加上一些個人加上的註解方便理解。
延遲加鎖機制
讓我們試想一下,如果一張表上面有多個索引,這樣在異動其中一筆資料的時候,豈不是要在好幾個 B+Tree 上同時加鎖嗎 ?
加鎖也是一筆開銷,如果衝突的可能性很小的時候,多數的鎖應該都是不必要的。
Innodb 實現了一個延遲加鎖機制,以此來減少加鎖的數量,減少效能損耗的同時也提升併發性能。
隱式鎖的簡介
Innodb 實現的延遲加鎖的機制,在源碼中被稱為
隱式鎖(implicit lock)隱式鎖沒有實際加鎖,而是一種
logical entity,transaction 是否持有是透過一些過程來計算的。 隱式鎖中有一個重要的元素:
db_trx_id!用來儲存產生該筆紀錄的 transaction ID,這是在 MVCC 中實現 快照讀 也有使用到的元素。隱式鎖的特點
- 只有在很可能發生衝突時才加鎖,減少鎖的數量
- 隱式鎖式針對 B+Tree 上被修改的紀錄,因此都是
record lock,不可能是gap lock或next-key lock
- 和顯式鎖的區別:
隱式鎖的使用
INSERT操作一般只會加隱式鎖,不加顯示鎖。除了以下情況:- 若
INSERT的位置有gap lock時,則會加上insert intention lock。 - 若
INSERT的位置發生唯一鍵衝突時,則會將對方的隱式鎖升級為顯示鎖,自己則加上shared lock等待。




UPDATE和DELETE操作在查詢時,會直接對查詢走的index和primary key使用顯示鎖,其他的index使用隱式鎖。
隱式鎖的具體過程
- 如果 transaction 要獲取行鎖 (不論是顯式或隱式),則需要先判斷是否存在活躍的隱式鎖
- cluster index:首先取得在該紀錄上持有隱式鎖的 transaction id,隨後透過 cluster index 中的隱藏欄位
db_trx_id判斷前者是否為活躍事務。 - secondary index:
- 從 secondary index page 中取得
PAGE_MAX_TRX_ID (T1) - 取得 InnoDB 活躍中事務中
最小的 Transaction ID (T2) - 假如 T1 < T2 則說明修改這個 page 的 T1 已經 commit ,因此不存在隱式鎖。

PAGE_MAX_TRX_ID:該字段存在於 secondary index page 中,用於保存修改該 page 的最大 Transaction ID,當該 page 的任何紀錄被更新後,都會更新此值。
反之,則必須再透過 cluster index 搭配 undo log 回溯舊版本數據進行判斷,此步驟較為複雜暫不詳解 (可透過
storage\innobase\row\row0vers.cc 中 row_vers_impl_x_locked 和 row_vers_impl_x_locked_low 了解詳細過程)

- 若為活躍事務,則為該活躍事務將
implicit lock轉換為explicit lock
另外可已注意到
implicit lock 轉換後的 explicit lock 不會有 gap lock 
- 等待加鎖成功後修改數據,並且將自己的 Transaction id 寫入
db_trx_id;或者是 timeout。
總結
對於低基數經常更新的欄位,因為有隱式鎖的存在應該評估是否有其必要加入索引,也許其他高基數的索引已經足夠縮減查詢範圍,此時不將低基數經常更新的欄位放入索引可以減少 deadlock 的發生。
如果真的需要添加,應該避免低基數經常更新的欄位在多個索引中出現,避免多個 update 查詢使用不同索引在後續將隱式鎖升級為顯式鎖而引發 deadlock 的狀況。