間隙鎖產(chǎn)生的背景
備注: 本文使用的 MySQL 版本是: 8.0.13
隔離級(jí)別:可重復(fù)讀(RR)
存儲(chǔ)引擎:Innodb
以下面的表為例子進(jìn)行說(shuō)明
CREATE TABLE `tb` ( `id` int(11) NOT NULL, `a` int(11) DEFAULT NULL, `b` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `a` (`a`)) ENGINE=InnoDB;
insert into tb values(0,0,0),(10,10,10),(20,20,20),(30,30,30),(40,40,40),(50,50,50);
間隙鎖的產(chǎn)生來(lái)自于 InnboDB 引擎在可重復(fù)讀的級(jí)別基礎(chǔ)上執(zhí)行當(dāng)前讀時(shí)出現(xiàn)的幻讀問(wèn)題。下面來(lái)分析一下幻讀的例子,假如沒(méi)有間隙鎖的話,那么會(huì)出現(xiàn)下面的現(xiàn)象:
| sessionA | sessionB | |
|---|---|---|
| T1 | begin; select * from tb where a = 10 for update; 返回:(10,10,10) |
|
| T2 | insert into tb value(2,10,11); | |
| T3 | select * from tb where a = 10 for update; 返回:(10,10,10),(2,10,11) |
|
| T4 | commit; |
如上表如示,是基于沒(méi)有間隙鎖的假設(shè),sessionA 事務(wù)內(nèi)執(zhí)行兩次相同的當(dāng)前讀返回的數(shù)據(jù)不一樣,出現(xiàn)幻讀的現(xiàn)象。因?yàn)?2,2,10)這條記錄在原本的數(shù)據(jù)并不存在,行鎖就鎖不住,因此誕生間隙鎖。
間隙鎖加鎖規(guī)則
間隙鎖和行鎖合稱 next-key lock,每一個(gè) next-keylock 是前開(kāi)后閉區(qū)間,如: (0,10]
原則 1:加鎖的基本單位是 next-key lock。
原則 2:加鎖是基于索引的,查找過(guò)程中訪問(wèn)到的對(duì)象才會(huì)加鎖
優(yōu)化 1:索引上的等值查詢,給唯一索引加鎖的時(shí)候,next-key lock 退化為行鎖
優(yōu)化 2:索引上的等值查詢,向右遍歷時(shí)且最后一個(gè)值不滿足等值條件的時(shí)候,next-key lock 退化為間隙鎖。
分析上面例子
select * from tb where b = 10 for update;
加鎖的基于索引的
因?yàn)?b 不是索引,索引走的是主鍵索引,加鎖就是加在主鍵索引上-
加鎖范圍
遍歷主鍵索引,發(fā)現(xiàn) b=10 時(shí),需要在前后記錄之間加鎖,所以在前一主鍵記錄(0,0,0)和本記錄之間加鎖(0,10],在后一條主鍵記錄(20,20)和本記錄之間加鎖 (10,20]。然后繼續(xù)向右遍歷,判斷 b=20,!=10,滿足優(yōu)化 2 規(guī)則,next-key lock 退化為間隙鎖,變成(10,20)。同時(shí) b=10 加了行鎖,匯總鎖范圍: (0,20),針對(duì) id 主鍵
-
間隙鎖與“往這個(gè)間隙插入操作”沖突
因?yàn)樯厦嬷麈I id 鎖的范圍是(0,20),因此插入(2,2,10)中 主鍵 2 屬于鎖范圍,因此阻塞
注意: 線程之間的間隙鎖是不沖突的
查詢條件走二級(jí)索引例子
| sessionA | sessionB | |
|---|---|---|
| T1 | begin; select id from tb where a = 20 for update; |
|
| T2 | 場(chǎng)景 1:insert into tb values(5,11,11);//阻塞 場(chǎng)景 2: insert into tb values(15,10,8);//阻塞 場(chǎng)景 3:insert into tb values(17,9,1);//成功 場(chǎng)景 4: insert into tb values(25,35,88);//成功 |
|
| T3 | commit; |
首先看看sessionA的執(zhí)行計(jì)劃,發(fā)現(xiàn)用到覆蓋索引
explain select id from tb where a = 20 for update;
因?yàn)闂l件走了 a 索引,查詢字段 id 在索引 a中,用到的覆蓋索引,索引在搜索過(guò)程中,只用到索引 a,所以只會(huì)在索引 a 中間隙鎖,同時(shí)命中條件對(duì)應(yīng)的主鍵也要加上行鎖,所以主鍵 id=20 被加了行鎖。
間隙鎖加在索引 a 上的范圍是(10,30),為了更加深度理解加鎖范圍,如下圖:

場(chǎng)景 1:插入到索引 a 時(shí),要插入是索引是(11,5),屬于(a=10,id=10)和(a=30,id=30)之間的鎖范圍,所以阻塞
場(chǎng)景 2、3、4 同理分析得出結(jié)論
查詢條件走主鍵索引例子
| sessionA | sessionB | |
|---|---|---|
| T1 | begin; select * from tb where a = 10 for update; |
|
| T2 | 場(chǎng)景 1:insert into tb values(5,11,11);//阻塞 場(chǎng)景 2: insert into tb values(15,10,8);//阻塞 場(chǎng)景 3:insert into tb values(17,9,1);//阻塞(主鍵上的鎖) 場(chǎng)景 4: insert into tb values(25,35,88);//成功 場(chǎng)景 5:insert into tb values(5,5,5);//阻塞(主鍵上的鎖) |
|
| T3 | commit; |
本例子跟《查詢條件走二級(jí)索引例子》區(qū)別在于 sessionA 是 select * ,因此需要回到主鍵索引查詢所有字段,掃描了主鍵索引,所以也會(huì)在掃描到的索引進(jìn)行加 next-key lock。該語(yǔ)句回表一次,掃描到是行是 id=10,所以加鎖是(0,10],(10,20),因此 sessionA 一共加了鎖是索引 a 的(10,30)和主鍵索引的(0,20)。
- 場(chǎng)景 1,2 跟上一個(gè)例子一樣命中索引 a 和主鍵索引的鎖范圍,阻塞
- 場(chǎng)景 3 因?yàn)?17 屬于主鍵索引(10,20)之間,所以被阻塞
- 場(chǎng)景 4 不用索引 a 和主鍵索引的鎖范圍,所以成功。
- 場(chǎng)景 5,沒(méi)命中索引 a 的鎖,但是命中了主鍵上的鎖范圍,所以被主鍵索引上的鎖阻塞