MySQL 核心模塊揭秘 — 什么時候釋放鎖?
1. 概述
InnoDB 事務執(zhí)行過程中,加表鎖或者行鎖之后,釋放鎖最常見的時機是事務提交或者回滾即將完成時。
因為事務的生命周期結束,它加的鎖的生命周期也隨之結束。
有一種情況,加鎖只是權宜之計,臨時為之。如果這種鎖也要等到事務提交或者回滾即將完成時才釋放,阻塞其它事務的時間也可能更長,這就有點不合理了。所以,這種鎖會在事務運行過程中及時釋放。
還有一種情況,雖然是在事務提交過程中釋放鎖,但是并不會等到提交即將完成時才釋放,而是在二階段提交的 prepare 階段就提前釋放。
最后,有點特殊的就是 AUTO-INC 鎖了。
2. 不匹配 where 條件
我們先來看看只是權宜之計的加鎖場景。
select、update、delete 語句執(zhí)行過程中,不管 where 條件是否命中索引,也不管是等值查詢還是范圍查詢,只要掃描過的記錄,都會加行鎖。
和 update、delete 不一樣,select 只在需要加鎖時,才會按照上面的邏輯加鎖。
可重復讀(REPEATABLE-READ)、可串行化(SERIALIZABLE)兩種隔離級別,只要加了鎖,不管是表鎖還是行鎖,都要等到事務提交或者回滾即將完成時才釋放(手動加的表鎖除外)。這就是我們前面說的釋放鎖最常見的時機了。
讀未提交(READ-UNCOMMITTED)、讀已提交(READ-COMMITTED)兩種隔離級別,如果發(fā)現記錄不匹配 where 條件,會及時釋放行鎖。這又分為兩種情況。
情況 1,如果部分或者全部 where 條件下推到了存儲引擎,InnoDB 每讀取一條記錄,都會判斷記錄是否匹配下推的 where 條件。
情況 2,server 層每次收到 InnoDB 返回的一條記錄,也會判斷記錄是否匹配 server 層的 where 條件。
以上兩種情況,只要記錄不匹配 where 條件,就會馬上釋放當前 SQL 語句對記錄加的行鎖(其實有個例外情況,稍后介紹)。
這里釋放行鎖,只會釋放不匹配 where 條件的這一條記錄上的行鎖,過程也比較簡單,就是把行鎖結構的 bitmap 內存區(qū)域中,這條記錄對應的位設置為 0。
如果有其它事務正在等待獲得這條記錄的行鎖,還會根據行鎖的授予規(guī)則,給其它事務授予鎖。
前面提到有一個例外情況,現在,該它出場了。
如果事務對某條記錄加行鎖,沒有立即獲得鎖,而是進入了鎖等待狀態(tài),等其它事務釋放鎖之后才獲得鎖。InnoDB 或者 server 層發(fā)現這條記錄不匹配 where 條件,并不會釋放它的行鎖。
這是為什么呢?
因這經過鎖等待狀態(tài)之后才獲得的行鎖,事務就不知道是哪條 SQL 語句執(zhí)行時給加的行鎖了,所以,即使發(fā)現記錄不匹配 where 條件,也不會釋放它的行鎖。
3. prepare 階段
讀未提交(READ-UNCOMMITTED)、讀已提交(READ-COMMITTED)兩種隔離級別下:
- select、update、delete 語句全表掃描、索引范圍掃描過程中,只會對索引記錄加普通記錄鎖,不會加間隙鎖和 Next-Key 鎖。
- 外鍵約束檢查、重復值檢查這兩個場景下,還是會對索引記錄加間隙鎖或者 Next-Key 鎖的。
這兩種隔離級對索引記錄前面間隙的鎖定,不需要等到事務提交或者回滾即將完成時才釋放,在事務二階段提交的 prepare 階段就可以提前釋放。
Next-Key 鎖既鎖定索引記錄本身,又鎖定索引記錄前面的間隙。
如果釋放索引記錄的 Next-Key 鎖,就意味著釋放了索引記錄本身和前面間隙的鎖定,這顯然是不行的,因為索引記錄本身的鎖定,要等到事務提交或者回滾即將完成時,才能釋放。
那么,Next-Key 鎖要怎么釋放?
稍安勿躁,我們一起來看。
釋放索引記錄前面間隙的鎖定,需要遍歷事務對象的 trx_locks 鏈表,遍歷過程中,每次取一個鎖結構(可能是表鎖結構或者行鎖結構)。
如果鎖結構對應的是間隙鎖,直接釋放索引記錄上的間隙鎖,主要流程如下:
- 從事務對象的 trx_locks 鏈表中刪除行鎖結構。
- 從 rec_hash 的數組中找到鎖結構所在的行鎖結構鏈表,然后從鏈表中刪除鎖結構。
- 鎖結構的 bitmap 內存區(qū)域中,可能有一個或者多個位的值為 1,這些位對應的記錄都被加了間隙鎖。如果有其它事務正在等待獲得這些記錄上的行鎖,根據行鎖的授予規(guī)則,給這些事務授予鎖。
如果鎖結構對應的是 Next-Key 鎖,只釋放索引記錄前面間隙的鎖定,保留索引記錄本身的鎖定,主要流程如下:
- 給鎖結構的 type_mode 屬性加上 LOCK_REC_NOT_GAP 標志,也就是直接把 Next-Key 鎖變成了普通記錄鎖。
- 鎖結構的 bitmap 內存區(qū)域中,可能有一個或者多個位的值為 1,這些位對應的記錄都被加了 Next-Key 鎖,現在都變成了普通記錄鎖。如果有其它事務想往這些記錄前面的間隙插入記錄被阻塞了,現在就可以根據行鎖的授予規(guī)則,給這些事務授予插入意向鎖了。
4. 事務提交或回滾
事務加行鎖的共享鎖、排他鎖之前,會分別加表級別的意向共享鎖、意向排他鎖,這兩種表鎖都要到事務提交或者回滾即將完成時才釋放。
事物加的所有行鎖,除了讀未提交(READ-UNCOMMITTED)、讀已提交(READ-COMMITTED)兩種隔離級別已經釋放的不匹配 where 條件的記錄上的行鎖、索引記錄前面間隙的鎖定之外,剩下的行鎖,都要等到事務提交或者回滾即將完成時才釋放。
事務提交或者回滾事務即將完成時,釋放表鎖和行鎖需要遍歷 trx_locks 鏈表,遍歷過程中,每次取一個鎖結構。
對于行鎖結構,釋放鎖的主要流程如下:
- 從事務對象的 trx_locks 鏈表中刪除行鎖結構。
- 從 rec_hash 的數組中找到鎖結構所在的行鎖結構鏈表,然后從鏈表中刪除行鎖結構。
- 鎖結構的 bitmap 內存區(qū)域中,可能有一個或者多個位的值為 1,這些位對應的記錄都被加了某種行鎖。如果有其它事務正在等待獲得這些記錄上的行鎖,根據行鎖的授予規(guī)則,給這些事務授予鎖。
對于表鎖結構,釋放鎖的主要流程如下:
- 從事務對象的 trx_locks 鏈表中刪除表鎖結構。
- 從表對象的 locks 鏈表刪除表鎖結構。
- 如果有其它事務正在等待獲得這個表的表鎖,根據表鎖的授予規(guī)則,給這些事務授予鎖。
5. AUTO-INC 鎖
把 AUTO-INC 鎖單獨拿出來說,是因為它有點特殊。
前面介紹 InnoDB 表鎖時,我們介紹過,AUTO-INC 鎖有兩種類型,一種是輕量鎖,這其實只是個互斥量。
另一種才是真正的表級別的 AUTO-INC 鎖,它會創(chuàng)建表鎖結構,和表級別的意向共享鎖、意向排他鎖一樣,都屬于表鎖。
AUTO-INC 鎖的這兩種類型,釋放時機不同。
輕量鎖,用完就釋放,也就是 insert 或者 update 語句獲取到自增列的自增值之后,就可以釋放了。
因為這種類型,只是個互斥量,釋放也很簡單,調用釋放互斥量的方法就可以了。
此時,可能有其它事務正在等待獲得這個表的輕量 AUTO-INC 鎖,也就是等待獲得這個互斥量,由操作系統決定哪個事務能獲得這個互斥量。
真正的表級別的 AUTO-INC 鎖,要等到加鎖的 SQL 語句執(zhí)行完成才釋放,主要流程如下:
- 從事務對象的 autoinc_locks 數組中刪除表鎖結構。
- 從事務對象的 trx_locks 鏈表中刪除表鎖結構。
- 從表對象的 locks 鏈表中刪除表鎖結構。
- 如果有其它事務正在等待獲得這個表的 AUTO-INC 鎖,根據表鎖的授予規(guī)則,給這些事務授予鎖。
6. 總結
事務執(zhí)行過程中加的表鎖,都要等到提交或者回滾即將完成時才釋放(手動加的表鎖除外)。
事務執(zhí)行過程中加的行鎖,根據事務隔離級別的不同,釋放時機不同。
可重復讀(REPEATABLE-READ)、可串行化(SERIALIZABLE)兩種隔離級別,事務加的所有行鎖,都要等到提交或者回滾即將完成時才釋放。
讀未提交(READ-UNCOMMITTED)、讀已提交(READ-COMMITTED)兩種隔離級別,事務加的行鎖,釋放時機不同。
- 對于不匹配 where 條件的記錄,發(fā)現不匹配之后,server 層或者 InnoDB 就會釋放這些記錄的行鎖。
- 對于間隙鎖或者 Next-Key 鎖,在二階段提交的 prepare 階段,會釋放記錄前面間隙的鎖定,保留記錄本身的鎖定。
- 剩余未釋放的行鎖,都要等到事務提交或者回滾即將完成時才釋放。
AUTO-INC 鎖有兩種類型,對應兩種釋放時機:
- 輕量鎖,用完就釋放。
- 真正的表級別的 AUTO-INC 鎖,加鎖的 SQL 語句執(zhí)行完成時釋放。