mysql innodb可重复读隔离级别下是如何实现避免幻读的

表象:快照读(非阻塞读)–伪MVCC

内在:next-key锁(行锁+gap锁)

快照读和当前读

当前读:select…lock in share mode, select… for update

当前读:update, delete, insert

其读取的是记录的最新版本,并且读取之后,还需要保证其他并发事务不能修改当前记录。

为什么update,delete,insert也是当前读呢?

以下面的update语句为例,

current read标明这就是当前读,return&lock返回,update row ,success 继续
在这里插入图片描述
快照读:不加锁的非阻塞读 select

目的是提高数据库的并发性

快照读的实现是基于多版本并发控制 Multi Version Concurrency Control

可以认为MVCC是行级锁的一个变种,在很多情况下避免了 加锁的操作,因此开销更低

快照读有可能读到的并不是数据的最新版本,可能是之前的历史版本

创建快照的时机决定了快照读的版本

在RR和Serializable下,真正防止幻读发生的是事务的next-key锁

next-key lock

next-key lock由两部分组成,record lock + gap lock

行锁就是record lock,是对单个记录行上的锁。

gap lock。gap就是索引树中,插入新纪录的空隙。而gap lock即锁定一个范围,但不包括记录本身,gap锁是为了防止同一事务的两次当前读,出现幻读的情况。因此我们抓重点,主要讨论加gap锁的情况。

案例分析

RR和Searializable下默认都支持gap锁。这里我们主要讨论一下RR下出现gap锁的场景。

在RR下,增删改的时候,当前读若要用到主键索引或者唯一索引会用到gap锁吗?视情况而定。

情况1:where条件全部命中,则不会用Gap锁,只会加记录锁

什么是全部命中?可以理解为精确查询的时候,所有记录都有,比如说select * from table where id in (1,3,5),此时id为1,3,5的数据均在此table中存在并且出现,就是全部命中。如果只查到了部分,如查到了1,3,并未查到5,则为部分命中。

试想,当我们获取到的数据具备唯一性,比如假设id是主键,或者唯一键,那么在事务A中,我们将id作为筛选条件去做当前读的时候,比如delete from table where id = 9,那么事务B此时新增的那条信息必然也会在这个当前读的范围之外,所以在事务B新增数据并且提交了之后,事务A再去做当前读,还是获取到原先的数据集,并不会产生所谓的幻读现象。所以此时加行锁就足够了。锁住这写id为9的特定行,就能防止另外的事务对该结果集做出的影响。也就是说没有加gap锁的必要。

需要注意的是,加锁的时候,如果我们走的是主键之外的索引,那么我们需要对当前索引以及主键索引上对应的记录都上锁。我们来补充一下加锁的一些具体情况。

CREATE TABLE `tb`(
  `name` VARCHAR(10) NOT NULL,
  `id` INT(2) NOT NULL DEFAULT '0',
  PRIMARY KEY(`name`),
  UNIQUE KEY `unique_id`(`id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8

在这里插入图片描述
name为主键,id为唯一索引

那么当我们执行delete from tb where id = 9,该如何进行加锁呢?

此时,由于id是unique索引,因此delete语句会选择走id这一列的索引去进行where条件的过滤,在找到id=9的记录之后,将这条记录加上行锁,record lock,同时会根据读到的name(主键列),去聚集索引(主键索引)将name=d对应的主键索引项也加上record lock。

为什么密集索引下的索引项也要加上行锁呢?试想一下,如果并发的一个sql是通过主键索引来更新的,update tb set id = 90 where name = d,此时,如果delete语句没有将主键索引项上的记录加锁,那么并发的这个update语句就会感知不到delete语句的存在,这样就违背了同一条记录上的更新或者删除需要串行执行的约束。

情况2:where条件部分命中或者全不命中,则会加gap锁

如果delete from tb where id in (7,9) 或者 delete from tb where id = 7,那么就需要加gap锁,把6到9这个区间通过record lock锁住

情况3:当前读走的是非唯一索引的情况

在这里插入图片描述
id是非唯一索引,此时就需要用gap来防止幻读的发生了

我们使用delete from tb1 where id = 9这个当前读来选出id=9的数据,如果只锁住选出的两行,那么此后,另外一个事务B,插入了id同样为9的数据,并提交,事务A再次用当前读选出id=9的数据,会取出3条。这样就会发生幻读。因此这个时候我们需要引入gap锁。

gap锁需要在什么地方添加呢?我们先来看看哪个地方算做是gap

gap和我们走的非唯一索引的分布有很大的关系,都是一个左开右闭的区间。

https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-gap-locks
在这里插入图片描述
上了gap锁,就没办法插入了,所以gap锁是用来防止插入的。只会对要操作数据的周边上gap锁。如上面的例子,会对6到9以及9到11来上gap锁,以预防幻读的发生。能够想象出来gap锁其实感觉锁定了B±tree中的一块

情况4:不走索引的情况,会对所有的gap都上锁,这也就类似于锁表了

在这里插入图片描述

相比表锁,这样上锁的代价更大,降低数据库的效率。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值