当前位置: 首页 > 科技观察

快速解开MySQL这把“锁”,拿下这7把钥匙,你就能把面试官撬下来

时间:2023-03-17 00:20:45 科技观察

前言MySQL作为目前使用最广泛的开源关系型数据库,是每个后端开发者都绕不开的一道坎。我在上一篇文章中也写了关于MySQL中MVCC的细节以及如何在每个隔离级别中使用MVCC。如果您有兴趣,可以查看一下。这篇文章与MySQL中的锁有关。锁是并发程序中最常用的方法之一,但是锁的滥用也会给程序性能带来很大的负担。而我们平时在使用MySQL进行增删改查操作的时候,并没有感觉到我们在使用锁。其实是因为MySQL已经帮我们使用了相关的锁。如果想知道我们平时使用的SQL语句中使用了哪些锁?它们是如何锁定的?这些锁的作用是什么?然后你可以继续阅读。普通锁InnoDB实现了标准的行级锁,行级锁有两种:共享锁(sharedlocks,以下简称S锁):意在共享。即允许多个事务对一条记录共同持有一把共享锁,主要用于读操作。独占锁(exclusivelock,以下简称X锁):意在排除。只允许一个事务对一条记录持有独占锁,主要用于更新和删除操作。如果你了解过Java中的JUC包,那么你会发现它有点像JUC中的读写锁ReentrantReadWriteLock。它们的目的是提高读操作的并发性。如果有一个事务T1在行r上持有S锁,而另一个事务T2同时想要获取行r上的锁,则T2获取一个不同的锁,将发生以下情况:rowrSlock,那么T2会立即拿到锁。如果T2想要获取r行的X锁,那么T2就会被阻塞,直到T1释放r行的S锁。如果有一个事务T1持有r行的X锁,同时另一个事务T2想要获得r行的锁,那么无论T2获得什么锁,它都会被阻塞。X锁和S锁的兼容性如下图所示:最左边是持有的锁,最上面是你要申请的锁。从图中可以看出,只要是和X锁相关的都会发生冲突,即会造成阻塞。意向锁InnoDB允许多种粒度的锁共存,所以会出现表锁和行锁共存的情况。为了让各种粒度的锁能够共存,InnoDB使用了意向锁。意向锁是表级锁,用于表示一个事务正在持有锁或者打算申请锁。意向锁有两种类型:共享意向锁(intentionsharedlock,以下简称IS):表示事务持有表中某行的共享锁或打算获取某行的共享锁。共享排他锁(意向排他锁,以下简称IX):表示一个事务持有一个表中某行的排他锁或打算获取某行的排他锁。IS和IX只是表达一个意思,除了全表请求,它们不会阻塞任何操作。它们的主要目的只是表明持有一个行锁,或者打算获取一个行锁。意向锁的使用规则如下:当事务获取表中的共享行锁时,需要先获取表中的IS锁或更高级别的锁。事务在获取表的排他行锁时,需要先获取表的IX锁。这里有一个很重要的点:只有在获取到表中行锁的时候,才需要先申请意向锁。如果您正在执行需要锁定整个表的语句,例如ALTERTABLE,则不需要申请意向锁。可以直接申请表级X锁。X锁、S锁、IS锁、IX锁在表级的兼容性如下:注意:这里说的X锁、S锁也是表级锁,不要想当然的认为是行级锁。为什么会出现意向锁?我们考虑以下场景(假设没有意向锁):一个事务A想要修改表t中的行r,所以A获得了行r上的X锁,事务A现在持有A行锁。这时,一个事务B想用ALTERTABLE语句修改表t的结构。该语句首先需要获取表t的X锁,但是此时事务B并不知道表中是否有行被锁,所以只能逐行遍历,然后对遍历的行进行加锁line,直到发现之前表中没有一行被锁定,这时就可以修改表的结构了。但是发现表中有些行已经被锁了,那么就不能修改表结构,需要等待这些锁被释放。这里有一个大问题。在最坏的情况下,需要遍历所有行才能知道某一行是否被锁定。这是非常耗性能的,意向锁可以解决这个问题。现在考虑在同一个场景下如何解决这个问题:事务A要修改表t中的行r,A首先需要获取表t的IX锁,成功获取IX锁后,再申请r行。申请成功,事务A现在持有两把锁,分别是表t的IX锁和行r的X锁。这时有一个事务B想用ALTERTABLE语句修改表t的结构。该语句需要获取表t的X锁。事务B可以通过检查表t是否有锁来判断表中的行是否被锁。当发现表t上有IX锁时,事务B会被阻塞,因为它知道表中的行已经被锁了,所以不能申请表t上的X锁。看上面的兼容表,我们也知道表级IX锁和表级X锁是有冲突的,所以正好对应了这个场景。记录锁记录锁是索引记录上的锁,换句话说,记录锁只锁定索引。每张表都必须有一个主键索引(用户自定义主键,唯一索引,隐式生成),主键索引中非叶子节点中的记录使用记录锁进行锁定。假设执行语句:select*fromuserwhereid=10forupdate;如果id是user表的主键,那么在主键索引中,id为10的记录将被锁定。而其他事务要更新或删除这条记录就会被阻塞,只有在这条记录中的记录锁被释放后才能进行其他操作。InnoDB中除了主键索引,还会有二级索引。二级索引与主键索引相同。当使用二级索引作为查询条件时,符合条件的二级索引的记录会被记录锁锁定,然后对应的主键索引在回表时也会被记录锁锁定。假设执行语句:select*fromuserwherename='c'forupdate;如果id是user表的主键,name是user表的二级索引。它会先锁定二级索引下name='c'的索引,然后回表锁定主键索引为9的主键索引。间隙锁间隙锁(简称Gap)是一种锁在索引记录之间的间隙上,或者在第一个索引记录之前的间隙和最后一个记录之后的间隙上锁定。间隙锁是防止幻读的主要手段之一。幻读是指同一个事务在不同的时间执行同一个查询语句,得到的结果集是不同的。那么间隙锁是如何防止幻读的呢?实际上,通过锁定指定的间隙,新记录无法插入这些间隙,从而阻止数据增长。假设我们执行这条语句:select*fromuserwhereid>5andid<9forupdate;由于间隙锁的存在,其他事务如果想插入id在5到9之间的记录是无法成功的,会一直阻塞Block,直到间隙锁被释放。比如你要插入一条id为6的记录,就会被阻塞,如下图(省略了一些不相关的字段)。间隙锁跨越的间隙可以是单个值、多个值,甚至是空值。从上图我们可以知道:(5,7]:id为5的索引记录和id为7的索引记录之间的间隙被gap锁锁定(7,9]:id为7的索引记录和id为9的索引记录之间的gap被gaplock锁定了,因为这两个gap被gaplock锁定了,所以两个gap之间的记录是不能插入的,只能在gaplock之后插入被释放了。我们还需要注意的是,id为7的记录是被recordlock锁定的,所以当对id为7的记录进行update和delete操作时,会被阻塞。我们上面也提到了gaplock还是在第一条记录的前面和最后一条记录的后面加锁,看看是怎么回事假设我们执行这条语句:select*fromuserforupdate;因为该语句没有使用索引,所以会执行一个全表扫描。将扫描到记录的每条记录都加锁,所有的间隙也加锁。fi最终的加锁情况如下图所示(省略了一些不相关的字段):每张表中都会隐含两条记录:最小记录(infimum),最大记录(supermum)通过上图我们可以得到如下的加锁范围:(-∞,5](5,7](7,9](9,10](10,12](12,+∞)并且所有记录都被记录锁锁定。这看起来像表锁,因为对表的任何操作(快照读取除外)都会被阻止。但是,间隙锁并不是在任何情况下都会用到,只有以下几种情况不会用到:隔离级别为RC、RU。使用唯一索引进行等价比较,得到一条索引记录。这是因为一个等价比较的唯一索引只能获取一条记录,而不是多条记录的情况下,不会出现多次读取不一致的情况。间隙锁的主要目的是防止事务向间隙中插入记录,间隙锁可以共存,多个事务可以同时获取获得相同间隙的锁。共享间隙锁和排他间隙锁之间没有区别,它们是完全一样的东西。Next-Key锁Next-Key锁并不是什么难理解的东西,它本质上是索引记录上的记录锁和索引记录间的间隙锁的组合。InnoDB在搜索和扫描表时,会为扫描到的记录加记录锁。记录锁可以是共享锁,也可以是排他锁。因此,行级锁实际上是索引记录锁。两个gap锁的例子中的第二个,其实就是Next-Key锁,因为括号内的每一个内存都包含了一个indexrecordlock和一个gaplock,非常符合Next-Key的定义。在默认的REPEATABLEREAD隔离级别下,InnoDB在搜索和扫描索引时使用Next-Key锁来防止幻读。InsertintentlockInsertintentlock(简称IIGap)是一种特殊的间隙锁,只在插入记录时使用。这个锁表示插入的意图。与上面提到的表级意向锁完全不同。插入意向锁属于行级锁,相互兼容不冲突。因此多个事务可以同时获取同一个gapIIGap锁。.官方示例:假设有值为4和7的索引记录,单独的事务分别尝试插入值5和6,每个事务使用插入意向锁在4和7之间加锁,然后再获取插入的排他锁它们之间有行间隙,但不要互相阻塞,因为行是无冲突的。插入意向锁只与间隙锁和下一键锁冲突。因为间隙锁的主要作用是防止幻读的发生,而在执行插入操作之前需要获取插入意向锁,而插入意向锁和间隙锁之间存在冲突,可以阻止插入操作,所以间隙锁可以防止幻读。阅读发生了。AUTO-INC锁AUTO-INC锁也叫自增锁(简称AI锁)。它是一种特殊的表锁,在向具有AUTO_INCREMENT列的表中插入数据时使用。当插入数据的表中有自增列时,数据库需要自动生成自增值。在生成之前,它会先获取相关表的AUTO-INC锁。其他事务的插入操作会被阻塞,从而保证自增值的唯一性。AUTO-INC锁有以下特点:每个表都有自己的AUTO-INC锁,互不兼容。它不遵循两阶段锁定协议。事务提交时不释放,插入语句执行完毕后释放,提高了并发插入的性能。一旦分配了自增值,它就会增加一个。即使回滚,自增值也不会减一,而是会继续使用下一个值,所以自增值不一定是连续的。因为插入的时候会用到表锁,必然会造成并发插入的性能下降。因此,InooDB提供了一个innodb_autoinc_lock_mode配置项来控制自增锁的算法,让用户可以选择如何在自增值的可预测顺序和插入操作的最大并发度之间进行权衡。这个配置有三个选项:0:使用传统的锁方式,并发性能最差。1:默认使用的模式。2:并发性能最高,但不能保证同一条insert语句中的自增值是连续的。要了解有关此配置的更多信息,请查看此MySQL文档。总结一下InnoDB的四种行锁的兼容性,如下表所示:注意:第一列表示已经持有的锁,第一行表示需要获取的锁。从表中可以看出,插入意向锁不会影响其他事务获取其他锁。插入意向锁受Gap锁和Next-Key锁的影响。如果一个事务要获取指定间隙的插入意向锁,则间隙中的Gap锁和Next-Key锁不能被其他事务持有,否则会被阻塞。如果去掉插入意向锁的影响,那么兼容性表如下:从表中我们可以得出如下结论:当两个事务的锁涉及到记录锁时,就会发生冲突。间隙锁不会和其他锁冲突(插入意向锁除外)。作者:奋斗小皇帝链接:https://juejin.im/post/5ef6d8355188252e5961a253来源:掘金