type
status
date
slug
summary
tags
category
icon
password

前言

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

重現

notion image
notion image

單獨測試

  1. 查看兩句語法單獨執行的 LOCK 狀況
    1. notion image
      notion image
  1. 先執行 Transaction 1 再執行 Transaction 2
    1. notion image
  1. 先執行 Transaction 2 再執行 Transaction 1,可以發現 Transaction 2 多了一個 LOCK
    1. notion image
經由以上測試,我們確認了此例的 Lock 的成因如下表格-
notion image
在分析個過程中,我們可以看到當優先執行 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 中實現 快照讀 也有使用到的元素。

隱式鎖的特點


  1. 只有在很可能發生衝突時才加鎖,減少鎖的數量
  1. 隱式鎖式針對 B+Tree 上被修改的紀錄,因此都是 record lock,不可能是 gap locknext-key lock
  1. 和顯式鎖的區別:

    隱式鎖的使用


    1. INSERT 操作一般只會加隱式鎖,不加顯示鎖。除了以下情況:
        • INSERT 的位置有 gap lock 時,則會加上 insert intention lock
        notion image
        notion image
        • INSERT 的位置發生唯一鍵衝突時,則會將對方的隱式鎖升級為顯示鎖,自己則加上 shared lock 等待。
        notion image
        notion image
    1. UPDATEDELETE 操作在查詢時,會直接對查詢走的 indexprimary key 使用顯示鎖,其他的 index 使用隱式鎖。

    隱式鎖的具體過程


    1. 如果 transaction 要獲取行鎖 (不論是顯式或隱式),則需要先判斷是否存在活躍的隱式鎖
        • cluster index:首先取得在該紀錄上持有隱式鎖的 transaction id,隨後透過 cluster index 中的隱藏欄位 db_trx_id 判斷前者是否為活躍事務。
          • notion image
        • secondary index:
            1. 從 secondary index page 中取得 PAGE_MAX_TRX_ID (T1)
              1. PAGE_MAX_TRX_ID:該字段存在於 secondary index page 中,用於保存修改該 page 的最大 Transaction ID,當該 page 的任何紀錄被更新後,都會更新此值。
            1. 取得 InnoDB 活躍中事務中 最小的 Transaction ID (T2)
            1. 假如 T1 < T2 則說明修改這個 page 的 T1 已經 commit ,因此不存在隱式鎖。
              1. 反之,則必須再透過 cluster index 搭配 undo log 回溯舊版本數據進行判斷,此步驟較為複雜暫不詳解 (可透過 storage\innobase\row\row0vers.ccrow_vers_impl_x_lockedrow_vers_impl_x_locked_low 了解詳細過程)
            notion image
            notion image
    1. 若為活躍事務,則為該活躍事務將 implicit lock 轉換為 explicit lock
      1. 另外可已注意到 implicit lock 轉換後的 explicit lock 不會有 gap lock
        notion image
    1. 等待加鎖成功後修改數據,並且將自己的 Transaction id 寫入 db_trx_id;或者是 timeout。

    參考

    PPT