有小伙伴在微信上说,面试的时候被问到什么是Next-KeyLock,一头雾水,那么今天我们就来看看MySQL中的记录锁,间隙锁定和下一键锁定。1.RecordLockRecordLock也就是我们所说的记录锁。记录锁是索引记录上的锁。注意,它是针对一条索引记录的,即只锁住记录的那一行数据。例如下面的SQL:select*fromuserwhereid=1forupdate;注意id是一个索引,如果id不是索引,上面SQL加的排他锁就不是RecordLock。我们看下面的例子:首先,我们将系统变量innodb_status_output_locks设置为ON,如下:接下来,我们执行如下SQL,锁定一行数据。这时会自动给表加一个IX锁:接下来,我们在一个新的session中执行如下命令查看InnoDB存储引擎的情况:showengineinnodbstatus\G输出了很多信息,我们关注TRANSACTIONS,如下:可以看到:TABLELOCKtabletest08.usertrxid3564804lockmodeIX:这句话的意思是事务id为3564804的事务给user表加了意向排他锁(IX)。RECORDLOCKSspaceid851pageno3nbits80indexPRIMARYoftabletest08.usertrxid3564804lock_modeXlocksrecbutnotgap:这是一个锁结构的记录,这里的索引是PRIMARY,加的锁也是positive记录的记录锁(不是间隙)。看到LOCKSRECBUTNOTGAP意味着这是一个记录锁。那么这个RecordLock和我们之前讲的S锁、X锁有什么区别呢?S锁是共享锁,X锁是排它锁。当我们加S锁或者X锁的时候,如果使用索引,把锁加在具体的一条记录上,那么这个锁也是记录锁(其实就是记录锁,S锁,X锁,概念有有些重复,但描述的重点不同)。或者可以这样理解,记录锁又细分为S锁和X锁。它们之间的兼容性如下图所示:兼容性S-typerecordlocksX-typerecordlocksS-typerecordlockscompatibleandnotcompatibleX-typerecordlocksnotcompatiblewith2.GapLockGapLock也叫GapLock缝隙锁。它的存在可以解决幻读问题。另外需要注意的是,GapLock只在REPEATABLEREAD隔离级别下有效。让我们来看看什么是幻读。我们看下表:有两个session,A和B,首先在sessionA中开启一个事务,然后查询年龄为99的用户总数。注意使用currentread,因为在default中在隔离级别下,默认的快照读取无法读取其他事务提交的数据。关于snapshotread和currentread的区别可以参考:S锁和X锁,currentread和snapshotread!。在sessionA中第一次查询后,在sessionB中的数据库中添加了一行记录。当在sessionA中进行第二次查询时,结果与第一次查询不同。这就是幻读(注意幻读特指数据插入导致的不一致)。在MySQL默认的隔离级别REPEATABLEREAD下,无法重现上述情况。之所以无法复现,是因为在MySQL的REPEATABLEREAD隔离级别中,已经帮我们解决了幻读问题,解决方案就是GapLock。大家想一想,造成幻读问题的原因是记录之间存在空隙,而用户可以在这些空隙中插入数据,从而导致幻读问题,如下图所示:,ids之间是有差距的,有差距的地方,就有漏洞。前面我们提到的记录锁只能锁定一条特定的记录,但是对于记录之间的间隙是无能为力的,从而导致幻读(其他事务可以向间隙插入数据)。现在GapLock的间隙锁就是锁住这些记录之间的间隙。如果缝隙被锁住,就不用担心幻读的问题了。这也是GapLock存在的意义。向记录添加间隙锁可锁定记录前面的间隙。例如对id为1的记录加GapLock,锁定范围为(-∞,1),对id为3的记录加GapLock,锁定范围为(1,3)、那么id为10后的间隙如何锁定呢?MySQL提供了一个Supreme来表示当前页中最大的记录,所以Supreme最终锁定的范围是(10,+∞),这样,所有的间隙都被覆盖了,由于间隙被锁定了,所以都是打开间隔。那么我们如何才能看到间隙锁呢?让我给你一个简单的例子,假设我有下表:CREATETABLE`user`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`username`varchar(255)COLLATEutf8mb4_unicode_ciDEFAULTNULL,`age`int(11)DEFAULTNULL,PRIMARYKEY(`id`),KEY`age`(`age`))ENGINE=InnoDBAUTO_INCREMENT=5DEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ci;一张简单的表,id为主键,age为普通索引,表中有如下记录:接下来我们执行如下SQL锁定一行数据,此时也会产生间隙锁:接下来,在一个新的session中,我们执行如下命令查看InnoDB存储引擎的状态:showengineinnodbstatus\G输出了很多信息,我们关注TRANSACTIONS,如下:红框选中的,就是间隙锁的锁记录。可以看到在某段录音前加了一个gaplock。这是间隙锁。大家一定要牢记:GapLock只在REPEATABLEREAD隔离级别下有效。3.Next-KeyLock以下内容基于MySQL默认的隔离级别REPEATABLEREAD。如果我们既要锁定一行又要锁定行与行之间的记录,那就是Next-KeyLock。换句话说,Next-KeyLock是RecordLock和GapLock的组合。通常我们行锁的基本单位是Next-KeyLock,即既有记录锁也有间隙锁,但是有时候Next-KeyLock会退化。我们通过几个简单的例子来分析一下。首先我们看一下Next-KeyLock的加锁规则:锁的范围是左开右闭。如果是唯一非空索引的等价查询,Next-KeyLock会退化为RecordLock。对于普通索引的等价查询,向后遍历时,当最后一个不满足等价条件时,Next-KeyLock会退化为GapLock。让我们通过几个简单的例子来分析一下。3.1唯一非空索引假设我有一个学生表,上面有学生的姓名和成绩,如下所示:NULL,`score`doubleNOTNULL,PRIMARYKEY(`id`),UNIQUEKEY`score`(`score`))ENGINE=InnoDBAUTO_INCREMENT=4DEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ci;id是主键,score是等级,其中score是唯一的非空索引。现在表中有如下数据:假设我们执行如下SQL:在本例中,由于score是唯一的非空索引,Next-KeyLock会退化为RecordLock。也就是说,这行SQL只给得分为90的记录加锁,没有GapLock,也就是我们新开一个session插入一条得分为88的记录也是OK的。但是,这里有一个特例。如果加锁了一条不存在的记录,也会产生间隙锁,比如下面:由于没有得分为91的记录,所以这里会产生范围为(90,95)的间隙锁,我们可以通过执行如下SQL来验证:可以看到90.1和94.9都会被屏蔽(我按了CtrlC,所以大家可以看到查询终止了)。90和95不符合唯一非空索引的条件。95.1可以插入成功。没问题。3.2非空索引现在我们重新开始,将分数索引改为普通索引,如下:NULL,`score`doubleNOTNULL,PRIMARYKEY(`id`),KEY`score`(`score`))ENGINE=InnoDBAUTO_INCREMENT=8DEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ci;数据还是和之前一样,现在我们执行如下SQL:分析一下吧。此时id为90的记录要加锁,所以先加一个gaplock,最后的score是89,所以这次加gaplock的范围是(89,90),id为record90条需要同时加锁,所以进一步优化为(89,90)。同时这里还有一个规则,就是最后一条满足条件的记录也需要加锁,所以最终锁定范围是[89,90],由于score不是唯一索引,所以需要继续向后查找,找到的下一条记录是95。由于此时Next-KeyLock会退化为GapLock,锁定范围为(90,95),综上所述,最终锁定范围为[89,95)。接下来,我们可以打开一个新的session,我们尝试添加如下数据,看是否添加成功:可以看到,88分是可以的,89.1分就不行了。95分可以,但94.9分就不行了。再次尝试89是否OK:说明我们上面分析的锁定范围是正确的。我们看下面的SQL:和前面的case相比,这次多了一个limit1,也就是说只有一条记录,所以这次搜索到90之后就不再继续搜索了,那么最后的锁是一个间隙锁+A记录锁,最终范围为[89,90]。这时,打开一个新的session,分别插入得分为88.9、89、90、91的记录,验证一下我们上面分析的锁定范围:88.9、89的插入结果和我们的预期是一致的。可以看出这里也可以插入90,之所以可以插入是因为90之后没有间隙锁。4.总结MySQL中的锁有点复杂。小伙伴们可以在某个周末抽点时间过一遍,这样以后在面试中遇到这些问题就不会不知所措了。
