MySQL锁这块石头似乎没有我想的那么重
前言
前言为本人写这篇文章的牢骚,建议跳过不看。
之前好几次都想好好的学习MySQL中的锁,但是找了几篇文章,看了一些锁的类型有那么多种,一时间也没看懂是什么意思,于是跟自己说先放松下自己,便从书桌起来在阳台发呆、做做运动、打扫下公寓,时间就这样过去了,真就印证了那句话:“当你在学习的时候,无论做什么其他的事情都是有趣的。”但却又能安慰自己:”我不是不想学习,只是在学习的过程中放松了下自己,下次再看吧吧”,这样心里也不会存有罪恶感,安稳睡去,下次亦是重复如此。但是日志一长,MySQL锁就变成了一块又硬又重的石头,完全不想碰它。但是这几天重新看了一遍,发现这块石头减肥了,于是便有了这篇文章。
MySQL的锁
首先在阅读文章之前,得先理解一个概念:锁之间的互斥是作用于获取锁来说的。举个例子,我拿到了一条记录的X锁,如果其他线程想要获取这条记录的其他锁,那么就需要等待,这就是互斥;但是如果其他线程不想获取锁,只是简单的访问,例如说select * from t where id = 1这些不用锁的语句,并不需要获取此记录的锁,那么这个SQL不会进入阻塞,并不会形成互斥。
共享锁/独占锁(Shared and Exclusive Locks)
一种比较大的分类,不一定指行锁,也可能是表锁。如果是X锁(独占锁)则跟其他所有锁形成互斥;如果是S锁(共享锁),则可实现读读(SS)并行。哈?你叫我举个例子。okay,拿行锁(记录锁)来说,当我们执行下面的SQL时当前事务拿到的是id为3的X行锁。
update t set name = 'name1' where id = 3;
那么代表什么呢,代表如果上方的事务还没结束的话,此时下面的操作都会被阻塞。
// 获取记录上的S锁
select * from t where id = 3 lock in share mode;
// 获取记录上的X锁
update t set name = 'name2' where id = 3;
也就是说,共享锁和独占锁的互斥关系如下表。
X | S | |
---|---|---|
X | 互斥 | 互斥 |
S | 互斥 | 兼容 |
但是,这个时候我有一个问题。
如果执行的这个语句呢?
select * from t where id = 3;
是否还会出现阻塞的情况呢?好好的思考一下,然后想想自己为什么要想这个没有意义的问题。
答案是不会出现阻塞的情况,原因我在一开始的时候就说了,互斥是针对锁的争夺来说的,上面的这条SQL并不需要获取到锁,所以也就不会进入阻塞。
意向锁(Intention Locks)
在讲意向锁之前,我们首先思考这样的一个问题:
如果我想对整张表进行独占式锁定(即X表锁),我没办法像个莽夫一样直接就锁上了,我得知道表中的记录有没有被其他的事务锁住,如果有的话那么我就进入阻塞等待释放,那要怎么知道呢?
暴力点从第一条记录开始遍历到最后?不行,如果一张表有几百万甚至上千万的话效率就会非常低下(别跟我说该分表了)。于是,为了解决这种性能问题,意向锁应援而生。
意向锁就是为了提升获取表锁前判断而存在的锁。首先意向锁是表级别锁(表锁跟行锁是可以同时存在不冲突),在获取数据行上的独占锁或者共享锁之前首先得获取到表级别的意向锁,这样子,当有其他线程想要进行锁表的时候,看到已经存在意向锁,那么就进入阻塞状态,而不用遍历所有数据行判断是否行上有锁,这样子就大幅提升了性能。
意向锁也分共享意向锁(IS)和独占意向锁(IX)。意思也很简单,获取S锁之前需要获取的就是IS锁,获取X锁之前需要获取的就是IX锁,其锁之间的互斥关系如下图。
X | S | |
---|---|---|
IX | 互斥 | 互斥 |
IS | 互斥 | 兼容 |
看了上面的图,可能有人会问:”兄弟,你这只有跟独占/共享锁的互斥图,意向锁之间的互斥关系呢?”
首先,我得跟你说,我绝对不是因为懒得画图才不画的,因为意向锁之间是不互斥的。你想嘛,本来意向锁就是为了提升锁表时的判断性能而存在的,而且本身自己也是表级别的锁,你还给整个互斥关系,到时候不给你堵得像北京三环一样,所以不互斥是一级棒的。
记录锁(Record Locks)
在上方也简单的提到过记录锁,记录锁也称行锁,是一种针对特定索引上的锁,注意是索引,而且如果只有记录锁的话必须是唯一索引(意思就是说只有记录锁一种,而非临建锁),过程为通过唯一索引定位到对应的聚簇索引,然后将这条聚簇索引锁住。如果唯一索引是多列组成的,但是查询条件只用到了其中几列,这种情况就不是施加记录锁,比方说,唯一索引为uniq_index(name,age)
,但是条件为where name = ‘name1’,这个时候用到的就不是记录锁而是临建锁了。
间隙锁(Gap Locks)
对于一些非唯一索引的查询或者范围查询的情况下,使用到的就是间隙锁。怎么理解呢,来看下官方给出的文档:
A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record.
意思就是说间隙锁是在索引之间、第一个索引之前或者最后一个索引之后的锁。这样讲可能还是有点抽象,举个简单的例子,假设现在有这样的一张表:
id(bigint) | name(varchar) |
---|---|
3 | n3 |
5 | n5 |
7 | n7 |
再结合上面那句话的意思,那么可以知道这张表中间隙锁的取值范围为
(负无穷,3),
(3,5),
(5,7),
(7, 正无穷)
有些文章的范围会将右边边界也取上,但我觉得这样反而不符合官方文档的定义,而且不好理解,不过取上也没关系,因为间隙锁一般不会单独使用,一般使用的都是临键锁(临键锁的话会把记录带上,也就是边界也取到了),所以锁的范围都是一样的,看个人觉得哪个容易理解。另外关于间隙锁的范围,官方描述中有一段有趣的描述,是这样的:
A gap might span a single index value, multiple index values, or even be empty.
意思就是说,间隙可以是一个索引、可以跨越多个索引,甚至可以是空的;换句话说,”根本就没有间隙锁,或者说,哪里都是间隙锁。”
另外需要注意:间隙锁只有在RC级别及以上的隔离级别中才有,其他级别是没有间隙锁的。
讲了这么多可能还是云里雾里的,来举个简单的例子,还是上面表格中的记录。
id(bigint) | name(varchar) |
---|---|
3 | n3 |
5 | n5 |
7 | n7 |
id的值分别为3、5、7,那么下面这条SQL的间隙范围是多少呢?
select * from t where id between 3 and 5 for update;
你以为是(3,5)?其实是(3,5)和(5,7)哒,这也另一方面证实了官方说的间隙锁可能跨越索引的说法。实际上由于临键锁的存在,会把记录锁也带上,也就是边界3、5、7都会被锁住(即范围为[3,7],已验证,若有其他见解请在评论区狠狠的打我的脸,不要客气,打得越狠越好)。
讲到这里,可能有人会说,”你说的这些我现在懂了,那这个间隙锁到底有什么用呢?”
emmm,先看下官方文档是怎么说的:
Gap locks in
InnoDB
are “purely inhibitive”, which means that their only purpose is to prevent other transactions from inserting to the gap.
用我的工地英语翻译过来就是,间隙锁不会阻塞,唯一的作用就是防止其他事务往这个间隙中插入记录;毫不客气的说,间隙锁除了防止在间隙之间插入记录之外一无是处,间隙锁之间甚至还可以共存(无论是X还是S间隙锁),你锁(3,5)跟我锁(2,6)又有什么关系呢?
临键锁(next-key locks)
上方介绍间隙锁的时候多多少少也提到过,临键锁就是记录锁+间隙锁的结合。一般来说使用的都是临键锁而非间隙锁,上方的SQL:
select * from t where id between 3 and 5 for update;
锁定的范围带上边界也说明了,实际上边界上的锁都是记录锁,所以范围才变成了[3, 7]。当然使用的是唯一索引的话会进化成记录锁(多列索引的情况需另外考虑,记录锁处已提到),下面这个SQL用的就是记录锁。
select * from t where id = 3 for update;
插入意向锁
一种特殊的间隙锁,在其间隙中可以进行非当前记录的插入,目的是为了提高插入的并发,可以理解为间隙为1的间隙锁。 同时也是一种意向锁,只不过这是给插入专用的。
自增锁
表锁,一般用在设置了auto_increment
的字段。大致就是先获取锁,然后将最大的ID自增,与其他锁不同的时候,完成自增操作之后其就释放了,不需要等待事务的完成。
本文为博客园原创文章。
参考:
//blog.csdn.net/qq_41026740/article/details/97408858
//zhuanlan.zhihu.com/p/48269420
//dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-insert-intention-locks