你们不仁,我也不能不义,今天含泪(捂脸)继续分享手撕MySQL系列-锁机制。
先来看一下面试中可能会导致你被pass的问题:
什么是MySQL的锁,它有几种类型?锁的优化策略有哪些?
锁的定义
锁是计算机协调多个进程或线程并发访问某一资源的机制。锁保证数据并发访问的一致性、有效性;Mysql锁在服务器层和存储引擎层进行并发控制。
MySQL InnoDB 锁的基本类型
这里为什么要单独拎出InnoDB的锁类型加以讲解,大部分情况下我们使用的MySQL引擎是InnoDB,他的锁类型相对于MyISAM也较复杂。MyISAM只有表级别的Read和Write锁。
官网把锁分成了 8 类。
我们把前面的两个行级别的锁(Shared and Exclusive Locks),和两个表级别的锁(Intention Locks)称为锁的基本模式。
后面三个 Record Locks、Gap Locks、Next-Key Locks,我们把它们叫做锁的算法, 也就是分别在什么情况下锁定什么范围。
这么多洋文码子,你可能又说老子都不想看了,都不知道什么意思,安排!
Shared and Exclusive Locks (共享和排他锁)Intention Locks (意向锁)Record Locks (记录锁)Gap Locks (间隙锁)Next-Key Locks (临键锁)是不是有些关键字你听说过了?
在分析他们之前我们再来补充一点基础知识。
锁的粒度
InnoDB里既有行级别的锁,又有表级别的锁;表锁,顾名思义,是锁住一张表;行锁就是锁住表里面的一行数据。看一下这两种锁的一些差异
锁定粒度,表锁肯定是大于行锁的。加锁效率,表锁是大于行锁的。为什么?表锁只需要直接锁住这张表就行了,而行锁,还需要在表面去检索这一行数据,所以表锁的加锁效率更高。冲突概率,表锁大于行锁,因为当我们锁住一张表的时候,其他任何一个事务都不能操作这张表。但是 我们锁住了表里面的一行数据的时候,其他的事务还可以来操作表里面的其他没有被锁 定的行,所以表锁的冲突概率更大。表锁的冲突概率更大,所以并发性能更低,表锁的并发性能是小于行锁的。上面已经说了,InnoDB既支持行锁也支持表锁,这是从锁定粒度(或者说是锁定范围)上划分的,那么从功能方式上我们再来看看锁的别名们。
先来看看行锁的两种方式。
共享锁 (Shared Locks)
第一个行级别的锁就是我们在官网看到的 Shared Locks (共享锁),我们获取了 一行数据的读锁以后,可以用来读取数据,所以它也叫做读锁,多个事务可以共享一把读锁。
灵魂一问:你知道怎么给一行数据加上读锁吗?
我们可以用 select …… lock in share mode; 的方式手工加上一把读锁。
释放锁有两种方式,只要事务结束,锁就会自动事务,包括提交事务和结束事务。
开始手撕,我们用nivacat实际操作看一下,读锁是不是可以重复获取。
图1
图1我们对id=1的数据加上了一把读锁,事务并没有关闭,此时再开启一个查询界面,再次对该行数据获取读锁。
图2
图2可以看到,是没有问题的,可以对一行数据重复加读锁。
排他锁(Exclusive Locks)
第二个行级别的锁叫排它锁,它是用来操作数据的,所以又叫做写锁。只要一个事务获取了一行数据的排它锁,其他的事务就不能再获取这一行数据的共享锁和排它锁。
排它锁的加锁方式有两种,第一种是自动加排他锁。我们在操作数据的时候,包括增删改,都会默认加上一个排它锁。
还有一种是手工加锁,我们用一个 FOR UPDATE 给一行数据加上一个排它锁,这个无论是在我们的代码里面还是操作数据的工具里面,都比较常用。
再来验证以下写锁:
图3
如图3对id=1的数据加上一把写锁,事务并没有结束,此时再次对id加读锁。
图4
此时可以看到图4右上角红框位置出现了一朵菊花表示加载中,其实就是线程被阻塞掉了,因为图3的事务还没有结束释放写锁。
结论:对一条数据加写锁之后则不再允许其他事务对该行加读锁。
再来看看加写锁会怎么样:
图5
同样被阻塞,结论:对一条数据加写锁之后则不再允许其他事务对该行加写锁。
我们尝试删除他试一下:
图6
图6的菊花依然在,还是被阻塞。
总结以下:只要一个事务获取了一行数据的排它锁,其他的事务就不能再获取这一行数据的共享锁和排它锁。
行锁看完了,再来看一下表锁。
意向锁(Intention Locks)
意向锁,听起来好像很厉害的样子。它是由MySQL维护的,当我们给一行数据加上共享锁时,数据库会自动在这张表上面加一个意向共享锁。当我们给一行数据加上排他锁之前,数据库会自动在这张表上面加一个意向排他锁。
换一个方式说,你来品一品表级别的意向锁它有什么用。
如果一张表上至少有一个意向共享锁,说明有其他的事务给其中的某些数据行加上了共享锁。
如果一张表上至少有一个意向排他锁,说明有其他的事务给其中的某些数据行加上了排他锁。
你品,你细品。
我来帮你品一品。
第一、有了表级别的锁,在 InnoDB 里面就可以支持更多粒度的锁(这看起来像是废话)
第二、如果说没有意向锁的话,当我们准备给一张表加上表锁的时候,我们首先要去判断有没其他的事务锁定了其中了某些行,这个时候我们就要去扫描整张表才能确定能不能成功加上一个表锁,如果数据量特别大,比如有上千万的数据的时候,加表锁的效率就会很低。
第三、引入了意向锁之后就不一样了。我只要判断这张表上面有没有意向锁,如果有,就直接返回失败。如果没有,就可以加锁成功。所以 InnoDB 里面的表锁,可以把它理解成一个标志。就像火车上厕所有没有人使用的灯,是用来提高加锁的效率的。
好了,锁的四种基本模式,讲完了:
行锁:共享锁,排他锁;
表锁:意向共享锁;意向排他锁;
锁了半天,你可能觉得,哦,我懂了,行锁锁的是行,表锁锁的是表,很简单嘛!老子聪明极了。
真是这样吗?
注意:以下内容可能会引起不适,建议提前看博主的另一篇手撕B+树的文章【当我们聊数据库索引时,我们该聊些什么】
当然如果你对索引很熟悉,继续看吧少年。
先撸一个没有主键的表:
图7
图8
图8是表中的数据,注意id并不是主键。
尝试对id=5的数据加写锁。
图9
理论上我们加的是行级写锁,只会锁住id=5的一行数据,那来尝试锁一下id=10的数据;
图10
what?? 右上角菊花出现了,被阻塞了??
明明是行锁,为啥锁住了其他行,其实你可以试一下,整张表都被锁住了。
不卖关子了,这张表博主一开始就说了,它没有主键,也就是说你以为的id=5在MySQL看来并不能唯一命中聚集索引的主键,当表没有主键时,MySQL其实是为表创建了隐式的_rowId来作为聚集索引的主键,当你使用id=5为条件为数据加锁时,实际上MySQL只能全表扫描去找到它,所以只能锁表。
加上主键(过程略),我们再来看看。
图11
先对id=1加写锁,再来对id=2加写锁
图12
没问题,菊花没出现,是行锁本锁没错了。
扩展一下,如果对图12中的表,加上一个name的辅助索引,我们使用
begin;select * from user where name = 'Bill' for update;
加上一把锁,此时锁住的是什么?欢迎留言交流。
以下是MySQL加锁的算法,有兴趣可以继续手撕。
Record Locks(记录锁)
当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,精准匹配到一条记录的时候,这个时候使用的就是记录锁。比如 where id = 1,如图11、12,已经演示过了。使用不同的id加锁,不会冲突,只会锁住这个记录。
间隙锁(Gap Locks)
当我们查询的记录不存在,没用命中任何一条记录时,无论是用等值查询还是范围查询的时候,它使用的都是间隙锁。重复一遍,当查询的记录不存在的时候,使用间隙锁。间隙锁主要是阻塞插入 insert。相同的间隙锁之间不冲突。
上图:先上一把锁
图13
尝试insert数据
图13
图13 这时候想去插入id=7的数据,菊花出现了,被阻塞了,你可能会说,你锁的就是id=7的数据啊,好,我们再来试一下
图14
如图,表中数据并没有id为2和3的数据
图15
图15,这时候我们插入id=3的数据,被阻塞了。
图16
如图16所示,间隙锁会把(没有结束的)事务查询没有结果的区间(左开右开)锁住。图14的查询条件也可以换成id=3,效果一样。
间隙锁只在 RR中存在。如果要关闭间隙锁,就是把事务隔离级别设置成 RC, 并且把 innodb_locks_unsafe_for_binlog 设置为 ON。
Next-Key Locks (临键锁)
当我们使用了范围查询,不仅仅命中了 Record 记录,还包含了 Gap 间隙,在这种情况下我们使用的就是临键锁,它是 MySQL 里面默认的行锁算法,相当于记录锁加上间隙锁。
它的退化情况:唯一性索引,等值查询匹配到一条记录的时候,退化成记录锁。 没有匹配到任何记录的时候,退化成间隙锁。
举个例子
图17
对于图17表,使用查询
图18
命中了一条数据,此时再次尝试一下锁住的区间,就不一一带着尝试了,试过之后你会发现;
它会锁住(1, 5] 和(5,7]区间, 也就是说,临键锁会锁住命中的最后记录的下一个左开右闭区间。
还记得什么是幻读吗?读到了其他事务已提交的新增记录的情况叫幻读。试想一下,当你查询时,临键锁已经把你的范围锁定了,其他事务自然无法提交你查询范围内的新增记录,幻读也就迎刃而解了。
锁的优化建议
使用锁,就自然会遇到死锁的情况,如何避免死锁在操作系统级别也给出了答案,一定避免环路等待,就是你等待我,同时我又等待你的情况,合理顺序访问;