暂无图片
暂无图片
2
暂无图片
暂无图片
暂无图片

MySQL之锁详解(三):InnoDB的Record锁、Gap锁和Next-Key锁

GrowthDBA 2022-06-07
6223

这是锁系列第4篇文章,今天我们介绍InnoDB存储引擎的Record Lock(记录锁)、Gap Lock(间隙锁)和Next-Key Lock(临键锁)。

之前的文章我们学习了MySQL Server层的MDL(元数据锁),InnoDB存储引擎层的S(共享锁)、X(排他/独占锁)、IS(意向共享锁)和IX(意向排他锁),错过的小伙伴可以移步至《MySQL之锁详解(一):MDL(元数据锁)、latch(闩锁)》《MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁》

开始今天的内容吧!~


Record Lock(记录锁)




01

基础知识

BASE

InnoDB存储引擎支持标准的行级锁,意向锁不会阻塞除全表以外的任何请求。与此同时,InnoDB存储引擎支持三种行锁的算法,分别如下:

※ Record Lock:单个索引记录上的锁;

 Gap Lock:间隙锁,锁定一个范围,但不包含记录本身;

 Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身。

『锁』离不开事务,事务(Transaction)包含4种隔离级别,不同隔离级别下会出现以下问题:

MySQL默认隔离级别是RR(可重复读)。由上图可知,RC(读已提交)隔离级别会出现幻读(phantom read)现象,是因为在RC隔离级别下,只有Record Lock(记录锁),没有Gap Lock(间隙锁)和Next-Key Lock(临键锁)。所以,为了区分这三种锁算法,我们需要区分事务隔离级别来看。同时,生产环境使用最多的隔离级别是RC和RR,所以我们只针对这两种事务隔离级别进行锁分析。

02

记录锁

REC LOCK

前面我们知道了,锁的类型主要分为S(共享)和X(排他),共享的时候大家都可以读取、不能改;排他的时候,除了占有排他锁的事务可以读写,其他事务均不能读、不能改,所以后面我们就不再赘述这部分内容,大家也可以回忆一下锁兼容矩阵。
下面我们就来看看记录锁。还是拿我们之前一直沿用的测试表test_lock.l举例。

调整当前Session的事务隔离级别,并以for update的方式查询全表数据,看看加锁情况:
mysql> SET tx_isolation='READ-COMMITTED';
mysql> SELECT @@tx_isolation;

mysql> BEGIN;
mysql> SELECT * FROM test_lock.l for update;

mysql> SHOW ENGINE INNODB STATUS\G

1、调整事务隔离级别。

2、以for update方式查询test_lock.l表的数据。

3、查看当前加锁情况。

上篇文章我们学习了持有锁信息的解读,所以是不是通过这个输出信息可以快速得到我们想要的信息:
2 lock struct(s), heap size 1136, 4 row lock(s):2个锁结构(TABLE LOCK和RECORD LOCKS),4行被锁住;
TABLE LOCK table `test_lock`.`l` trx id 3373879 lock mode IX:test_lock.l表上表级别的IX(意向排他)锁;
RECORD LOCKS space id 142 page no 3 n bits 72 index PRIMARY of table `test_lock`.`l` trx id 3373879 lock_mode X locks rec but not gap:要申请锁的这行记录在space id(表空间编号)为142,page no(页号)为3,在表`test_lock`.`l`上的index PRIMARY(聚簇索引)上,锁的类型是X(排他)锁,locks rec but not gap表示锁住的是记录而没有间隙,这句话就表示当前我们是记录锁,这个地方需要大家特别注意一下,后面学习到间隙锁的时候再对比下这个信息的不同之处。
后面输出的内容就是详细的加锁记录信息了,即使转换成十六进制,也不难看出,加锁的记录就是我们表`test_lock`.`l`的4条记录,同时,我们也得到了4条记录的heap no分别为2、3、4、5。

03

行锁的结构

STRUCT

通过上面信息的输出,我们得到了行锁的加锁位置是在space id(表空间编号)为142,page no(页号)为3,heap no分别为2、3、4、5的记录上,同时,在MySQL源码中(storage/innobase/include/lock0priv.h),InnoDB存储引擎对行锁结构的定义如下:

可以看到,锁是根据Page的组织形式进行管理的,若要知道Page中某一条记录是否持有锁,则通过位图的方式,即位图中的值为1表示该记录已经持有锁,位图中的索引与记录的heap_no一一对应,因此上述数据结构中还隐含了lock bitmap的信息,由于lock bitmap是根据页中记录数量来进行分配内存空间的,所以不显示定义。变量n_bits表示需要占用多少位用于位图管理(持有锁输出信息的坑填完了)。由于Page中的记录可能会之后还会进行增加,因此这里额外预留LOCK_PAGE_BITMAP_MARGIN(64)个记录的位图信息。

这样说有点不好理解,举个例子说明一下。假设page no为3这个Page中有250行记录,变量n_bits=250+64=314,那么是即位图需要40个字节用于位图的管理(n_bytes=1+314/8=40)。若Page中heap_no为2、3、4、5的记录上都已经上锁,则Page中记录与内存数据结构lock_rec_t的关系如下图所示:

由图可见,在页142-3中,前4条用户记录都有锁,因此在对应的数据结构的lock_rec_t中对应的heap no位图值都为1。



Gap Lock(间隙锁)




01

什么是间隙锁

WHAT

“间隙、间隙...”,顾名思义就是空隙,对于索引记录来说,就是两个记录间的空隙。
间隙锁:即持有在间隙上的锁结构。间隙锁存在于RR(可重复读)隔离级别下,为防止幻读发生设计出来的一种手段,因RC(读已提交)隔离级别可能发生幻读,故RC隔离级别下没有间隙锁。同样拿test_lock.l表举例:

当前test_lock.l表有4条记录,分别为2、4、6、8,又因为字段a数据类型为INT(整型),并且是PRIMARY KEY(主键),即完整用户索引记录(即B+树Leaf叶子节点)是按照字段a来组织的,我们就得到了test_lock.l表完整索引记录的B+树的Leaf(叶子)节点的简易图示:

如图所示(红色三角形所指的位置)得到5个区间:(infimum,2)、(2,4)、(4,6)、(6,8)、(8,supremum),这些个区间就是间隙,间隙锁就是加在这些间隙上的。
RC隔离级别下没有Gap Lock(间隙锁),我们做个测试:
mysql> SET tx_isolation='READ-COMMITTED';
mysql> SELECT @@tx_isolation;
mysql> BEGIN;
mysql> SELECT * FROM test_lock.l WHERE a IN (3,5,7) for update;

mysql> SHOW ENGINE INNODB STATUS\G

1、调整Session的事务隔离级别为RC,并且使用for update的方式访问test_lock.l表“间隙”中的记录值3、5、7。

2、查看当前加锁情况。

通过输出的信息我们可以很直观的看到:
1 lock struct(s), heap size 1136, 0 row lock(s):只有1个锁结构,没有记录行被锁住;
TABLE LOCK table `test_lock`.`l` trx id 3373895 lock mode IX:唯一的锁结构就是test_lock.l表上表级别的IX(意向排他)锁。
通过我们的验证,证实了RC(读已提交)事务隔离级别下是不会在row-level级别上加间隙锁的

02

间隙锁

GAP LOCK

至此,我们知道了RC隔离级别下是没有Gap Lock(间隙锁)的。那么下面,RR(可重复读)事务隔离级别下的Gap Lock(间隙锁)就正式登场了。
同样使用上面的SQL语句,我们再来看下RR隔离级别下的加锁情况:
mysql> SET tx_isolation='REPEATABLE-READ';
mysql> SELECT @@tx_isolation;
mysql> BEGIN;
mysql> SELECT * FROM test_lock.l WHERE a IN (3,5,7) for update;

mysql> SHOW ENGINE INNODB STATUS\G

1、同样的方式,先调整Session的事务隔离级别为RR,再使用for update的方式访问test_lock.l表“间隙”中的记录值3、5、7。

2、查看当前加锁情况。

通过上面的信息,我们可以看到加锁的信息:
2 lock struct(s), heap size 1136, 3 row lock(s): 2个锁结构,3行被锁住;
TABLE LOCK table `test_lock`.`l` trx id 3373897 lock mode IX:万年不变的表级别IX(意向排他)锁;
RECORD LOCKS space id 142 page no 3 n bits 72 index PRIMARY of table `test_lock`.`l` trx id 3373897 lock_mode X locks gap before rec:前面的信息大家已经再熟悉不过了,重点在标红的部分locks gap before rec,见名知意就是:锁住的是以下记录(4、6、8)之前的间隙,锁类型是X(排他)锁。这里需要和记录锁的信息:locks rec but not gap(锁住的记录而没有间隙)区别开来,这里虽然显示的RECORD LOCKS(记录锁),但是实际上不是,只是锁住的对应3条记录前面对应的间隙,验证一下:

从上面的结果可以论证出:事务1的锁类型是RECORD LOCKS(记录锁),同时有locks gap before rec标识;我们又开启了事务2,对已有的记录(4、6、8)进行for update查询,操作并没有被阻塞,所以证明了事务1确实是只锁住了间隙,也就是间隙锁。因为InnoDB存储引擎支持标准的row-level锁,所以输出的锁信息稍有“歧义”的现象大家需注意。
这就是间隙锁,这回知道“间隙”一词的由来了吧。是不是很好理解
小提示
MySQL官网(https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-gap-locks)有一段对间隙锁的描述:

InnoDB存储引擎的间隙锁是“纯粹的抑制性”,这意味着它们的唯一目的是防止其他事务插入到间隙中。间隙锁是可以并存的,一个间隙锁被一个事务持有的时候不会阻止另一个事务对这个间隙持有间隙锁,前提是这个间隙是没有数据的(防止往这个间隙写数据,防止别的事务往这个间隙写数据发生幻读)。共享和独占间隙锁之间没有区别。它们彼此不冲突,并且执行相同的功能。



Next-Key Lock(临键锁)




01

什么是临键锁

WHAT

在MySQL官网中,有对临键锁的描述(https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-next-key-locks),详见下图。

临键锁是索引记录上的记录锁和索引记录之前的间隙上的间隙锁的组合,即:Next-Key Lock = Gap Lock + Record Lock,锁定的是一个范围,同时也会锁定记录本身。

Next-Key锁是索引记录锁加上索引记录前面的间隙上的间隙锁。如果一个会话在索引中的记录上具有共享或排他锁,则另一个会话不能在索引顺序中紧接之前的间隙中插入新的索引记录。

假设索引包含值10、11、13和20。此索引Next-key锁涵盖以下区间(通常是左开右闭):

(-∞/infimum, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞/supremum)

对于最后一个间隔,Next-key Lock将间隙锁(Gap Lock)定在索引中最大值以上,并且“supremum”伪记录(对应还有最小“infimum”伪记录,希望大家还记得)的值高于索引中的任何真实值。最高值不是真正的索引记录,因此,实际上,这个Next-key锁定仅锁定最大索引值之后的间隙。

InnoDB存储引擎的默认隔离级别是REPEATABLE READ。这种情况下,InnoDB使用Next-key锁在进行搜索和索引扫描时,也可以防止幻读(Phantom Read)。

至此,是不是发现了Next-Key Lock和Gap Lock的区别,同样都是区间,Next-Key Lock的区间是左开右闭且包含记录本身,而Gap Lock是开区间且不包含记录本身

02

临键锁

NEXT-KEY LOCK

“没有什么问题是一个实验解决不了的,如果有,那就多做几个实验。

同样,我们使用RC和RR隔离级别分别看下同样SQL语句下的加锁情况。还是先从RC隔离级别开始。

mysql> SET tx_isolation='READ-COMMITTED';
mysql> SELECT @@tx_isolation;
mysql> BEGIN;
mysql> SELECT * FROM test_lock.l WHERE d = 10 for update;

mysql> SHOW ENGINE INNODB STATUS\G

1、调整Session的事务隔离级别为RC,并且使用for update的方式访问test_lock.l表d=10(注:d字段没有索引)的这行记录


2、查看当前的加锁情况。


2 lock struct(s), heap size 1136, 1 row lock(s):2个锁结构,1行数据被锁;

TABLE LOCK table `test_lock`.`l` trx id 3373913 lock mode IX万年不变的表级别IX(意向排他)锁;

RECORD LOCKS space id 142 page no 3 n bits 72 index PRIMARY of table `test_lock`.`l` trx id 3373913 lock_mode X locks rec but not gap:通过下面的记录信息和locks rec but not gap发现,锁住的是d=8这条完整的记录。这时你会发现,并没有提示锁住“间隙”的相关信息,所以,我们暂时又得到了一个结论:RC隔离级别下,也不加Next-Key Lock,直接加Record Lock

其实上面暂时得到的结论好像没有什么说服力,所以,再来做一个实验。

mysql> SET tx_isolation='READ-COMMITTED';
mysql> SELECT @@tx_isolation;
mysql> BEGIN;
mysql> SELECT * FROM test_lock.l for update;

mysql> SHOW ENGINE INNODB STATUS\G

1、调整Session的事务隔离级别为RC,并且使用for update的方式访问test_lock.l表所有记录(全表扫描)

2、查看当前加锁情况。

从输出的信息不难看出:同样是2个锁结构:test_lock.l表上的IX表级别锁,还有完整4行记录的Record Lock。这样就是证实了上面的结论:RC隔离级别下,不加Next-Key Lock,直接加Record Lock,即使全表扫描,也不加“表锁”,而是加所有记录完整的Record Lock(InnoDB存储引擎支持标准的行级别锁)。

下面再来看下RR隔离级别。

mysql> SET tx_isolation='REPEATABLE-READ';
mysql> SELECT @@tx_isolation;
mysql> BEGIN;
mysql> SELECT * FROM test_lock.l WHERE d = 10 for update;

mysql> SHOW ENGINE INNODB STATUS\G

1、调整Session的事务隔离级别为RR,并且使用for update的方式访问test_lock.l表d=10(注:d字段没有索引)的这行记录

2、查看当前加锁情况。

2 lock struct(s), heap size 1136, 5 row lock(s):2个锁结构,5行记录被锁住(其中包含supremum“伪记录”);

TABLE LOCK table `test_lock`.`l` trx id 3373916 lock mode IX万年不变的表级别IX(意向排他)锁;

RECORD LOCKS space id 142 page no 3 n bits 72 index PRIMARY of table `test_lock`.`l` trx id 3373916 lock_mode X:这里的显示内容既没有locks rec but not gap,也没有locks gap before rec,其实这就是Next-Key Lock了。我们验证一下。

在上述加锁的前提下,我们又开启了一个新事务,使用“间隙”中的值(d=9)进行for update访问,被阻塞了。证明这个间隙确实也被锁住了。所以也就验证了Next-Key Lock = Gap Lock + Record Lock,锁住记录的本身,也会锁住对应记录前的间隙。




小结




今天我们学习了记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock),是不是感觉有点意犹未尽,不要着急,锁系列文章仍会继续。对今天的内容做一个简单的总结 :

  • RECORD LOCKS ... lock_mode X:表示键锁,Next-Key Lock

  • RECORD LOCKS ... locks rec but not gap:表示记录锁,Record Lock

  • RECORD LOCKS ... locks gap before rec:表示间隙锁,Gap Lock

  • Gap Lock(间隙锁)锁住的区间均为开区间,Next-Key Lock(临键锁)锁住的区间除了“supremum”伪记录‍‍‍‍‍‍‍‍‍‍‍‍‍所在区间是开区间外,其余区间均为左开右闭区间;

  • RC(读已提交)隔离级别下,只有Record Lock,不会出现Gap Lock和Next-Key Lock。RR(可重复读)隔离级别下,按照非索引字段检索数据时,最小的加锁单位是Next-Key Lock;

  • 行锁结构是通过space、heap_no、n_bits组成的位图方式进行管理的,持有锁的记录在对应的数据结构lock_rec_t中对应的heap no位图值都为1。

今天的文章用比较通俗的话和实验讲解了InnoDB存储引擎层行锁的结构,也知道了同一SQL语句在不同隔离级别下加锁情况是不一样的。一般情况下,部分企业为了提高并发,生产环境会采用RC隔离级别。在RR隔离级别下,对没有索引的字段进行操作时,即使查询1行记录,也都会加所有记录的Next-Key Lock,这样看来,加锁的代价较RC隔离级别还是很大的。

其实,如果通过唯一、辅助索引操作数据的话,RR隔离级别下,加锁代价也是会有很大改善的,为什么说索引是把“双刃剑”,占用空间的情况下,一是可以加快查询速度,提高SQL执行性能,同时也会降低加锁代价,这就是我们后面文章的内容了,先简单剧透一下

下篇文章我们继续聊INSERT INTENTION LOCK(插入意向锁)和AUTO-INC LOCK(自增锁),后面还有死锁、锁实验、锁问题等内容,大家拭目以待。

今天的内容就到这里吧,站在巨人的肩膀上,每天进步一点点!~




参考资料

  • 姜承尧《MySQL技术内幕:InnoDB存储引擎 第2版》

  • 姜承尧《MySQL内核:InnoDB存储引擎 卷I》

  • https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html



扫描二维码关注

获取更多精彩

GrowthDBA


end


文章转载自GrowthDBA,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论