本篇作為學習筆記,文章內(nèi)容來自“極客時間”專欄《MySQL實戰(zhàn)45講》,如有侵權(quán),請告知,必即時刪除。
為了便于說明問題,建表和初始化語句如下:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
行鎖只能鎖住行,但是新插入記錄這個動作,要更新的是記錄之間的“間隙”。因此,為了解決幻讀問題,InnoDB 只好引入新的鎖,也就是間隙鎖 (Gap Lock)。
顧名思義,間隙鎖,鎖的就是兩個值之間的空隙。比如文章開頭的表 t,初始化插入了 6 個記錄,這就產(chǎn)生了 7 個間隙。
這樣,當你執(zhí)行 select * from t where d=5 for update 的時候,就不止是給數(shù)據(jù)庫中已有的 6 個記錄加上了行鎖,還同時加了 7 個間隙鎖。這樣就確保了無法再插入新的記錄。
也就是說這時候,在一行行掃描的過程中,不僅將給行加上了行鎖,還給行兩邊的空隙,也加上了間隙鎖。
跟間隙鎖存在沖突關(guān)系的,是“往這個間隙中插入一個記錄”這個操作。間隙鎖之間都不存在沖突關(guān)系。
這句話不太好理解,我給你舉個例子:
這里 session B 并不會被堵住。因為表 t 里并沒有 c=7 這個記錄,因此 session A 加的是間隙鎖 (5,10)。而 session B 也是在這個間隙加的間隙鎖。它們有共同的目標,即:保護這個間隙,不允許插入值。但,它們之間是不沖突的。
間隙鎖和行鎖合稱 next-key lock,每個 next-key lock 是前開后閉區(qū)間。也就是說,我們的表 t 初始化以后,如果用 select * from t for update 要把整個表所有記錄鎖起來,就形成了 7 個 next-key lock,分別是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。
今天分析的問題都是在可重復讀隔離級別下的,間隙鎖是在可重復讀隔離級別下才會生效的。所以,你如果把隔離級別設(shè)置為讀提交的話,就沒有間隙鎖了。
加鎖規(guī)則
加鎖規(guī)則里面,包含了兩個“原則”、兩個“優(yōu)化”和一個“bug”。
- 原則 1:加鎖的基本單位是 next-key lock。希望你還記得,next-key lock 是前開后閉區(qū)間。
- 原則 2:查找過程中訪問到的對象才會加鎖。
- 優(yōu)化 1:索引上的等值查詢,給唯一索引加鎖的時候,next-key lock 退化為行鎖。
- 優(yōu)化 2:索引上的等值查詢,向右遍歷時且最后一個值不滿足等值條件的時候,next-key lock 退化為間隙鎖。
- 一個 bug:唯一索引上的范圍查詢會訪問到不滿足條件的第一個值為止。
案例一:等值查詢間隙鎖
第一個例子是關(guān)于等值條件操作間隙:
由于表 t 中沒有 id=7 的記錄,所以用我們上面提到的加鎖規(guī)則判斷一下的話:
- 根據(jù)原則 1,加鎖單位是 next-key lock,session A 加鎖范圍就是 (5,10];
- 同時根據(jù)優(yōu)化 2,這是一個等值查詢 (id=7),而 id=10 不滿足查詢條件,next-key lock 退化成間隙鎖,因此最終加鎖的范圍是 (5,10)。
所以,session B 要往這個間隙里面插入 id=8 的記錄會被鎖住,但是 session C 修改 id=10 這行是可以的。
案例二:非唯一索引等值鎖
第二個例子是關(guān)于覆蓋索引上的鎖:
這里 session A 要給索引 c 上 c=5 的這一行加上讀鎖。
- 根據(jù)原則 1,加鎖單位是 next-key lock,因此會給 (0,5]加上 next-key lock。
- 要注意 c 是普通索引,因此僅訪問 c=5 這一條記錄是不能馬上停下來的,需要向右遍歷,查到 c=10 才放棄。根據(jù)原則 2,訪問到的都要加鎖,因此要給 (5,10]加 next-key lock。
- 但是同時這個符合優(yōu)化 2:等值判斷,向右遍歷,最后一個值不滿足 c=5 這個等值條件,因此退化成間隙鎖 (5,10)。
- 根據(jù)原則 2 ,只有訪問到的對象才會加鎖,這個查詢使用覆蓋索引,并不需要訪問主鍵索引,所以主鍵索引上沒有加任何鎖,這就是為什么 session B 的 update 語句可以執(zhí)行完成。
但 session C 要插入一個 (7,7,7) 的記錄,就會被 session A 的間隙鎖 (5,10) 鎖住。
lock in share mode 只鎖覆蓋索引,但是如果是 for update 就不一樣了。 執(zhí)行 for update 時,系統(tǒng)會認為你接下來要更新數(shù)據(jù),因此會順便給主鍵索引上滿足條件的行加上行鎖。
這個例子說明,鎖是加在索引上的;同時,它給我們的指導是,如果你要用 lock in share mode 來給行加讀鎖避免數(shù)據(jù)被更新的話,就必須得繞過覆蓋索引的優(yōu)化,在查詢字段中加入索引中不存在的字段。比如,將 session A 的查詢語句改成 select d from t where c=5 lock in share mode。你可以自己驗證一下效果。
案例三:主鍵索引范圍鎖
第三個例子是關(guān)于范圍查詢的。
現(xiàn)在我們就用前面提到的加鎖規(guī)則,來分析一下 session A 會加什么鎖呢?
- 開始執(zhí)行的時候,要找到第一個 id=10 的行,因此本該是 next-key lock(5,10]。 根據(jù)優(yōu)化 1, 主鍵 id 上的等值條件,退化成行鎖,只加了 id=10 這一行的行鎖。
- 范圍查找就往后繼續(xù)找,找到 id=15 這一行停下來,因此需要加 next-key lock(10,15]。
所以,session A 這時候鎖的范圍就是主鍵索引上,行鎖 id=10 和 next-key lock(10,15]。這樣,session B 和 session C 的結(jié)果你就能理解了。
這里你需要注意一點,首次 session A 定位查找 id=10 的行的時候,是當做等值查詢來判斷的,而向右掃描到 id=15 的時候,用的是范圍查詢判斷。