手记

MySQL系列-仅靠MVCC就能解决幻读?错!Gap锁了解一下?

前言

上篇MySQL系列有提到间隙锁,但是我感觉没有讲清楚,仅仅只是介绍了一下间隙锁,所以再来记录一下间隙锁。

上篇传送门:juejin.cn/post/698622…

先说一下结论: 读,分为当前读快照读MVCC解决的是快照读的幻读问题,而当前读的幻读问题需要Gap锁+行锁来解决

一、Gap锁(间隙锁)的概念

Innodb支持三种行锁定方式:

  1. 行锁:锁的是索引,如果SQL没有走索引,那么会全表扫描,从而升级为表锁。

  • 如果有主键,Innodb会把主键作为聚簇索引。

  • 如果没有主键,Innodb会选择第一个不包含有 NULL 值的唯一索引作为主键索引

  • 如果没有主键且没有唯一索引,Innodb会选择内置的rowId(Innodb内部对每一行数据维护了一个递增的rowId)作为聚簇索引。

  • PS: 想必肯定有同学会想,如果某张表没有索引怎么办?

Gap锁(间隙锁):当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时(增删改查操作,Mysql会默认加锁),InnoDB会给符合条件的已有数据记录的索引项加行锁。对于键值在条件范围内但并不存在的记录加上间隙锁。Innodb 为了解决幻读问题时引入的锁机制,所以只有在 Read Repeatable 、Serializable 隔离级别才有。要取消间隙锁的话,切换隔离级别为读已提交即可。

  • PS: 如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!

Next-Key Lock:行锁与间隙锁组合起来用就叫做Next-Key Lock。

二、快照读和当前读

快照读: 就是普通的select语句,不包含for update的和lock in share mode的select语句。

当前读:读取的是最新的数据,并且需要先获取对应记录的锁,包含以下这些 SQL 类型:

  • select ... lock in share mode 

  • select ... for update

  • 增删改(MySQL默认加锁)

当前读是通过Next-Key Lock(行锁+间隙锁) 来实现的

下面来看个例子:

数据库的原始数据如下图

在RR(可重复读)隔离级别下开启两个事物进行操作:

咦。是不是有点奇怪,SQL1是快照读,SQL4是当前读,RR隔离级别下,在同一个事务中还是发生了幻读。

当时我就想了一下,不是说MVCC解决了快照读的幻读问题,间隙锁解决了当前读的问题吗。为什么我这里还是会出现幻读的问题呢?

是这样的,MySQL的当前读,是指在一个事务中的第一个SQL就是当前读了,也就是说第一个SQL就会加锁,其他的事务拿不到锁,根本没办法对对应的记录增删改。必须要等第一个事务释放锁之后,其他的事务才能对对应的记录增删改查。虽然解决了当前读的幻读问题,但是性能受到了影响。

三、间隙锁实例分析

建立一个user表的记录如下:

事务A执行delete from user where age=3, 而age=3位于age(2,7)之间, 所以MySQL对age(2,7)区间的数据加了间隙锁,当事务B对age(2,7)区间进行增删改操作时,会发生阻塞的现象。如图:

再看个例子,原始数据如下:

同理,当事务A执行update user set money=100 where id>=5时,MySQL会对id【5, 正无穷大)进行加锁,这时,事务B只要在id【5, 正无穷大)区间发生增删改操作,都会发生阻塞。因为对id=5的这一行数据加了行锁,并且对id>5的但还不存在的数据加了间隙锁。如图:

四、Gap锁造成的死锁

历史数据如图:

开启两个事务进行操作:

发生死锁的原因:

  1. age=5的数据不存在,即对age(2,7)加上了间隙锁。

  2. age=8的数据不存在,即对age(7,9)加上了间隙锁。

  3. 增删改查语句,MySQL会默认加锁,SQL3插入的是age=8, 而age(7,9)已经被加上间隙锁,所以SQL3阻塞,等待age(7,9)的间隙锁释放。

  4. 增删改查语句,MySQL会默认加锁,SQL3插入的是age=5,而age(2,7)已经被加上间隙锁,所以SQL4死锁,等待age(2,7)的间隙锁释放。从而发生死锁。


作者:Boom
链接:https://juejin.cn/post/6988887904651051044
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


0人推荐
随时随地看视频
慕课网APP