深入分析解读MySQL锁,解决幻读问题( 二 )


接下来再看一个场景:

深入分析解读MySQL锁,解决幻读问题

文章插图
 
这个例子中我们是把name索引锁住了,然后我们在事务B中通过主键索引只查id,这样就用到name索引了,但是最后发现也被阻塞了 。所以我们又可以得出下面的结论,MySQL索引不但锁住了辅助索引,还会把辅助索引对应的主键索引一起锁住 。
到这里,可能有人会有怀疑,那就是我把辅助索引锁住了,但是假如加锁的时候,只用到了覆盖索引,然后我再去查主键会怎么样呢?
接下来让我们再验证一下:
深入分析解读MySQL锁,解决幻读问题

文章插图
 
我们可以看到,就算只是用到了辅助索引加锁,MySQL还是会把主键索引锁住,而主键索引的B+树叶子节点中,又存储了整条数据,所以查询任何字段都会被锁定 。
到这里,我们可以明确的给锁到底锁住了什么下结论了:
结论InnoDB引擎中,锁锁的是索引:
  • 假如一张表没有索引,MySQL会进行锁表(其实锁住的是隐藏列ROWID的主键索引)
  • 假如我们对辅助索引加锁,那么辅助索引所对应的主键索引也会被锁住
  • 主键索引被锁住,实际上就等于是整条记录都被锁住了(主键索引叶子节点存储了整条数据)
行锁的算法之前介绍事务的时候我们提到了,MySQL通过加锁来防止了幻读,但是如果行锁只是锁住一行记录,好像并不能防止幻读,所以行锁锁住一条记录的话只是其中一种情况,实际上行锁有三种算法:记录锁(Record Lock),间隙锁(Gap Lock)和临键锁(Next-Key Lock),而之所以能做到防止幻读,正是临键锁起的作用 。
记录锁(Record Lock)记录锁就是上面介绍的,当我们的查询能命中一条记录的时候,InnoDB就会使用记录锁,锁住所命中的这一行记录 。
间隙锁(Gap Lock)当我们的查询没有命中记录的时候,这时候InnoDB就会加上一个间隙锁 。
深入分析解读MySQL锁,解决幻读问题

文章插图
 
从上面的例子中,我们可以得出结论:
  • 间隙锁与间隙锁之间不冲突,也就是事务A加了间隙锁,事务B可以在同一个间隙中加间隙锁 。(之所以会用到间隙锁就是没有命中数据的时候,所以并没有必要去阻塞读,也没有必要阻塞其他事务对同一个间隙加锁)
  • 间隙锁主要是会阻塞插入操作
间隙是如何确定的test表中有5条记录,主键值分别为:1,5,8,10,20 。那么就会有如下六个间隙:
(-∞,1),(1,5),(5,8),(8,10),(10,20),(20,+∞)
而假如主键不是int类型,那么就会转化为ASCII码之后再确定间隙 。
临键锁(Next-Key Lock)临键锁就是记录锁和间隙锁的结合 。当我们进行一个范围查询,不但命中了一条或者多条记录,且同时包括了间隙,这时候就会使用临键锁,临键锁是InnoDB中行锁的默认算法 。
注意了,这里仅针对RR隔离级别,对于RC隔离级除了外键约束和唯一性约束会加间隙锁,没有间隙锁,自然也就没有了临键锁,所以RC级别下加的行锁都是记录锁,没有命中记录则不加锁,所以RC级别是没有解决幻读问题的 。
临键锁在以下两个条件时会降级成为间隙锁或者记录锁:
  • 当查询未命中任务记录时,会降级为间隙锁 。
  • 当使用主键或者唯一索引命中了一条记录时,会降级为记录锁 。

深入分析解读MySQL锁,解决幻读问题

文章插图
 
上面这个例子,事务A加的锁跨越了(1,5)和(5,8)两个间隙,且同时命中了5,然后我们发现我们对id=8这条数据进行操作也阻塞了,但是9这条记录插入成功了 。
临键锁加锁规则临键锁的划分是按照左开右闭的区间来划分的,也就是我们可以把test表中的记录划分出如下区间:(-∞,1],(1,5],(5,8],(8,10],(10,20],(20,+∞) 。
那么临键锁到底锁住了哪些范围呢?
临键锁中锁住的是最后一个命中记录的 key 和其下一个左开右闭的区间
那么上面的例子中其实锁住了(1,5]和(5,8]这两个区间 。
临键锁为何能解决幻读问题临键锁为什么要锁住命中记录的下一个左开右闭的区间?答案就是为了解决幻读 。
我们想一想上面的查询范围id>=2且id<=6,如果我们事务A只锁住了(1,5]这个区间,假如这时候事务B插入一条数据id=6,那么事务A再去查询,就会多出来了一条记录id=6,就会出现了幻读,所以我把你下一个区间5,10]也给锁住,就可以避免了幻读 。


推荐阅读