MVCC與鎖

pinoky發表於2024-09-14

MVCC與鎖

鎖基本原理

當事務想要改動記錄時,會檢視記憶體中有沒有跟該記錄相關聯的鎖結構

  • 沒有的話就生成一個is_waiting為false的鎖結構與之關聯,代表獲取鎖成功;
  • 如果發現該記錄已經有鎖關聯了,會生成一個is_waiting為true的鎖結構,代表獲取鎖失敗,進入等待狀態;
  • 如果加鎖的事務結束,將釋放鎖結構,檢視是否有其他事務正在等待,若有則將其鎖的is_waiting改為false,並喚醒其事務對應執行緒喚醒

鎖定讀的語句:

  • SELECT ... LOCK IN SHARE MODE:事務執行該語句,則為讀取到的記錄加S鎖
  • SELECT ... FOR UPDATE :事務執行該語句,則為讀取到的記錄加X鎖

不同型別的鎖

  • 共享鎖:S鎖,讀取記錄前需要獲取s鎖

  • 獨佔鎖:X鎖,改動記錄前需要獲取X鎖

  • 意向共享鎖:當事務準備在某條記錄加上S鎖,需要在表級別加一個IS鎖

  • 意向獨佔鎖:當事務準備在某條記錄加上X鎖,需要在表級別加一個IX鎖

    意向鎖讓錶快速判斷表中記錄是否被加行鎖,為表是否能加表鎖提供依據,所以IX和IX,IX和IS鎖都是相容的,因為它們並不用作互斥

不同粒度的鎖

全域性鎖:對整個資料庫例項加鎖

典型應用場景是:做全庫邏輯備份,目的是讓備份系統備份得到的庫和原庫是保持邏輯一致性的,代價是如果在主庫上備份,期間不能做資料更新,業務停擺;如果在從庫上備份,備份期間從庫不能進行主從同步,導致延遲

如果引擎(innoDB)支援一致性讀,推薦使用single-transaction方法

但如果有的表使用了不支援事務的引擎,就需要對全庫加讀鎖,有以下兩種方式:

  • FTWRL:flush table with read lock
  • set global readonly = true

使用FTWRL更好,一是修改global的方式影響面更大;二是如果執行FTWRL之後客戶端異常斷開,則mysql會自動釋放全域性鎖,整個庫回到可以正常更新的狀態,但readonly而不會因為異常而取消readonly狀態

表級鎖:針對表加鎖

分為兩種:表鎖 和 後設資料鎖MDL

表鎖:lock tables .. read/write;一般在資料引擎不支援行鎖的時候才會用到

  • 可以使用unlock tables釋放鎖,或在客戶端斷開時自動釋放
  • 會限制其他執行緒 和 本執行緒 的讀寫

MDL:不需要顯式使用,在訪問一個表的時候會被自動加上

  • 作用:保證讀寫的正確性,比如禁止一個查詢正在遍歷表中資料,而執行期間另一個執行緒修改了表結構這種操作
  • MySQL5.5引入MDL,對一個表做增刪改查操作的時候,加 MDL 讀鎖;當要對錶做結構變更操作的時候,加 MDL 寫鎖
    • 讀鎖不互斥,允許多個執行緒同時對一張表增刪改查
    • 但讀寫鎖與寫鎖間互斥,保證變更表結構操作的安全性

自增鎖:用於AUTO_INCREATMENT修飾的列自動遞增,作用範圍是單個插入語句,執行插入語句時加一個AUTO_INC鎖,語句結束後釋放

行鎖

在innoDB事務中,行鎖在需要的時候加上,但等到事務結束時才釋放

所以要把最可能造成鎖衝突,最可能影響併發度的鎖儘量往後放

Record Locks,型別為LOCK_REC_NOT_GAP,有X鎖和S鎖,作用就是鎖一條記錄

Gap Locks間隙鎖:型別為LOCK_GAP,為解決幻讀問題發明的,X鎖和S鎖沒有差別,給一條記錄加gap鎖:其他事務不能在這條記錄前面的間隙插入新紀錄

可以透過給最後一條記錄A所在頁面的supremum記錄(該頁面中最大的記錄)加gap鎖,來阻止在A之後的間隙插入新記錄

幻讀:一個事務在前後兩次查詢同一個範圍的時候,後一次查詢看到了前一次查詢沒有看到的行(可重複讀隔離級別下,幻讀在“當前讀”下才會出現)

產生幻讀的一種原因:行鎖只能鎖住行,但新插入記錄這個動作,更新的是記錄的”間隙“,所以只好引入新的鎖解決這個問題:間隙鎖,它在可重複讀隔離級別下才有效

跟間隙鎖存在衝突關係的,是往這個間隙中插入一個記錄這個操作,而間隙鎖之間不存在衝突關係

間隙鎖+行鎖,合稱為next-key lock,每個next-key lock都是前開後閉區間(間隙鎖是開區間,加上一個行鎖後 就變成前開後閉)

但間隙鎖的引入,可能會導致同樣的語句鎖住更大的範圍,影響併發度

間隙鎖的加鎖規則:

  1. 原則 1:加鎖的基本單位是前開後閉區間的 next-key lock。
  2. 原則 2:查詢過程中訪問到的物件都會加鎖。(範圍查詢會繼續往後訪問,訪問到哪加鎖到哪)
  3. 最佳化 1:索引上的等值查詢,給唯一索引加鎖的時候,next-key lock 退化為行鎖。
  4. 最佳化 2:索引上的等值查詢,向右遍歷時且最後一個值不滿足等值條件的時候,next-key lock 退化為間隙鎖。
  5. 一個 bug:唯一索引上的範圍查詢會訪問到不滿足條件的第一個值為止

插入意向鎖:如果在被gap鎖鎖住的區域想插入記錄,該事務就會為gap鎖鎖住的記錄加上插入意向鎖,等待gap鎖釋放,它的作用僅限於此,不會阻止其他事務獲取任何型別的鎖

隱式鎖:INSERT語句一般不加鎖,但可以透過事務id,為新插入的記錄加隱式鎖

死鎖和死鎖檢測

死鎖:併發系統中不同執行緒出現迴圈資源依賴,導致這幾個執行緒都進入無限等待的狀態

出現死鎖後,有兩種策略:

  • 直接進入等待,直到超時,超時時間由:innodb_lock_wait_timeout決定,預設是50s,即當出現死鎖後,第一個鎖住的執行緒要過50s才會超時退出,後續的執行緒才有可能執行

  • 發起死鎖檢測,發現死鎖後主動回滾死鎖鏈條中的某個事務,讓其他事務得以繼續執行,將innodb_deadlock_detect設定為on開啟

    • 負擔:每當一個事務鎖住時,都需要判斷會不會由於自己的加入導致死鎖,這是一個O(N)的操作
    • 如果能確保業務一定不會出現死鎖,可以臨時關閉死鎖檢測;

檢視死鎖:show engine innodb status裡的LATESTDETECTED DEADLOCK 記錄最後一次死鎖資訊

MVCC版本控制

當我們在改動一條記錄時,該記錄的隱藏列roll_pointer指向undo日誌版本鏈的頭節點,trx_id記錄該版本鏈對應的事務id,這個版本鏈在MVCC多版本併發控制中發揮了很大的作用

對於READ UNCOMMITTED來說,髒讀是允許發生的,所以每次讀取資料時直接讀取最新版本即可

而對於READ COMMITTED和REPEATABLE READ來說,需要用到Readview來幫助進行版本控制,保證每次滿足不髒讀或可重複讀的需求

Readview:獲取當前系統活躍(尚未commit)的事務id列表、min_trx_id(最小的事務id),max_trx_id(系統應該分配給下一個事務的id),creator_trx_id(生成該Readview事務的id)

不髒讀即事務不能讀取到其他未提交事務修改的資料,觀察被訪問記錄當前版本的trx_id

  • trx_id == creator_trx_id:當前版本記錄就是當前事務,可以訪問該版本
  • trx_id < min_trx_id:小於當前活躍事務的最小id,說明該版本已經commit,可以訪問
  • trx_id > max_trx_id:說明當前事務執行時,該版本還沒有commit,不能訪問
  • 如果max_trx_id > trx_id > min_trx_id:檢視事務id列表裡有沒有當前記錄版本事務id,沒有就說明當前事務已經commit

就這樣順著版本鏈以此判斷當前版本是否對當前事務可見,就像是在生成 ReadView 的那個時刻做了一 次時間靜止(就像用相機拍了一個快照),查詢語句只能讀到在生成 ReadView 之前已提交事務所做的更改

而READ COMMITTED和REPEATABLE READ區別在於

  • 後者只會在第一次讀取資料時生成Readview,這就使得它在後續判斷版本可見性時用的都是最開始的Readview資料,所以即使在兩次讀取資料之間,有其他事務commit了,對於當前事務來說,那些事務commit的資料版本依舊是不可見的,這就實現了可重複讀的需求;
  • 前者則會每次讀取資料時,都生成一個新的Readview

事務利用MVCC進行的讀取操作叫做:一致性讀、一致性無鎖讀、快照讀

所有普通的select語句在READ COMMITTED和REPEATABLE READ下都是一致性讀,不會對記錄做任何加鎖操作,其他事務可以自由改動

相關文章