间隙锁的由来
在说间隙锁之前先搞清两个概念:快照读和当前读。
- 举个例子:一根电线上有很多鸟,如果让你数一下电线上站了几只鸟,怎么数?两种方法,第一先直接拍个照,到时候直接数照片上有几只,在数的过程中我也不管你现在电线上是不是又来了鸟,我只看照片上的,这就是快照读。无论让我数几次,这个数字都不会变,因为照片上的数量是在我拍照的那刻就决定了,这也就实现了可重复读(如果是RC级别,就是我想数的时候重新拍一下)。这种场景下就不会出现什么幻读了,因为你多还是少我压根不看,照片上总不能莫名其妙多出来一行吧?但是如果不让拍照,或者我必须要数最新的数量呢(当前读)?那我数的时候怎么避免没有鸟飞过来呢?那就只能用个罩子把它们盖起来,不允许其他鸟再来干扰,等数完再说。
- 回到mysql中,行锁其实挺好理解,毕竟一条数据不能被两个事务同时执行。
上图中id是主键索引,c是普通索引,d无索引。时间所限,我直接盗波图(出自丁奇mysql45讲)
事务A在三次查询时的结果由于是当前读,明显不一样(B和C默认事务自动提交)。第三次查出的结果变多,也就是出现了幻读。你就算锁了全部的记录,也架不住人家新增的数据啊,毕竟你锁的时候人还没出来呢。这时候就引出了间隙锁(Gap lock)的概念。比如把图中分为(-∞,0),(0,5),(5,10),(10,15),(15,20),(20,25),(25,+∞),凡是这个区间的都不允许插入,把行锁和间隙锁合并起来就成了一个前开后闭的区间 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum],这就叫next-key lock。
- 行锁是=,间隙锁是()这样的,结合起来就变成了next-key lock,格式就是(],因为锁的基本单位就是next-key lock,所以都从这个开始分析,一般说退化成行锁就是直接变=,退化成间隙锁就是变成()前闭后闭的。
间隙锁到底会锁哪些数据?
其实这个锁,是锁在索引上的,所以在没有索引的情况下,哪个区间也插不了数据,即锁表了。 我们就按照图中的两个字段来分,锁主键(或唯一索引),锁普通索引,下面的两个原则两个优化一个bug也不用理解,记住就完事了。(出自丁奇mysql45讲)
-
- 原则1:加锁的基本单位是next-key lock。希望你还记得,next-key lock是前开后闭区间。
-
- 原则2:查找过程中访问到的对象才会加锁。
-
- 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
-
- 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
-
- 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
- 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
- 先从主键索引开始,根据原则1,区间应该是(5,10],如果能直接查到这个主键,直接按3直接锁ID=7的行锁,不过既然没有,就按照4往后遍历,7后面是10,10很明显不满足等值条件(10!=7),那就退化成间隙锁就变成了(5,10),综合一下,结果就是凡是id在(5,10)的,都不能插入。
- 像这个就是非唯一索引,继续按套路,按原则1可知(0,5],不过普通索引可不是马上就停的,要继续往右遍历,直到碰到不满足的才行,也就是(5,10]也不行,别急,看看优化2怎么说的,最后一个值10满足=5?很明显跟上面的例子一样,退化成间隙锁(5,10),那综合起来就是(0,10);这个区间内不能插入,等等,到底是不能插入什么,这个区间其实就是锁c的,看下图
- 插入7,20,7,当c不在这个区间就可以插入,但是我插入8,7,7的时候就被阻塞了。那主键怎么没被锁住?事务A锁c=5的时候id不就是5吗?怎么事务B还能执行?看原则2,访问到的对象才会加锁,换个说法,锁是锁索引的这个是确定的,你用到那个索引才有可能被锁,事务A会用到主键索引吗?不会!为啥?覆盖索引的概念了,本身c索引里面带的就有id,不需要回表查询主键了,所以如果把select id 换成select d,事务B也不能执行了。哦,如果是select xx for update也会顺便把主键索引符合的加个锁,所以如果换成这种写法,事务B一样失败。
- 继续分析,主键范围索引,还是先按照原则1开始,找出(5,10],不过又触发了主键等值查询,所以直接定位到10的行锁,继续往右范围查找,也就是(10,15]的也不行,综合起来就是[10,15]
- 跟上图类似,不过不是主键了,普通索引的范围查询,继续分析,其实也没啥,多分析几遍,一共就那几个规则,按顺序来呗,照旧先来个(5,10],然后继续往右遍历得到(10,15],综合也就是(5,15]
- 继续分析,按照原则1得出(10,15],这个是大于并且没有等于,注意了。。往右推,按照第五条规则找到15还是符合条件的,要继续往下推,得到(15,20],综合起来就是(10,20],至于为啥说是bug,按理说唯一索引,找到15的时候就能知道不用往下推了,毕竟都等于15了,又是唯一的,还能找到小于等于15的值吗。。
总结
- 先看是不是主键,是主键就看是不是等值,等值如果存在就是行锁,不存在就查相应的区间锁,再根据优化2会退化成间隙锁(如锁的是id=7,从锁(5,10]的前开后闭,再优化成(5,10)),如果是范围主键,看条件有没有带等于,如果有等于会变成行锁继续往后推,直到碰到不满足条件的,注意这个不是等值判断了。所以也没必要退化成间隙锁,所以会锁[10,15],那如果条件是(id>10 and id<=15),就会触发那个bug,毕竟15也满足判断,所以还会继续下推到20。
- 如果不是主键看是不是二级索引,如果是二级索引的等值查询,其实就是往前推一个区间,再往后推一个区间,本身是(]这种结构,但是根据优化2,如果后一个区间的值不满足你的等值,那就回退化成间隙锁,也就变成了()这样的结构。如果是二级索引的范围查询,其实还差不多,继续往前推一个区间,再往后推,直到碰到不满足的,也就是(]这样的结构,他又不是等值查询,也不会退化间隙锁什么的。
- 如果没有索引,那就全部锁住,也不用算了。。