这是锁系列第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(记录锁)
基础知识
BASE
※ Record Lock:单个索引记录上的锁;
※ Gap Lock:间隙锁,锁定一个范围,但不包含记录本身;
※ Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身。
记录锁
REC 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
行锁的结构
STRUCT
这样说有点不好理解,举个例子说明一下。假设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的关系如下图所示:
Gap Lock(间隙锁)
什么是间隙锁
WHAT
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
间隙锁
GAP LOCK
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。
Next-Key Lock(临键锁)
什么是临键锁
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是开区间且不包含记录本身。
临键锁
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