MVCC底层原理

写在前面

  如果认为本文章对你有用,那可以为我点赞收藏加关注哦,因为创作不易,码字也不容易哦,点赞评论+关注是最好的支持。
 
 

说明

  在被面试官问面试题的时候,首先它问了Mysql的事务的隔离级别有几种?默认是哪种?他们分别解决了什么问题?

我在一顿回答“巴巴巴巴。。。。”之后,它又继续问题了我:
1、读已提交是怎么实现的呢?
2、可重复读能不能解决幻读?如果可以是怎么解决的?什么情况下又会出现幻读?

  我顿时不知道怎么回答,从这里,其实过后我才知道它已经把我从MySQL事务的隔离级别引到了MVCC那边,我需要去搞懂MVCC是什么以及它的底层原理。
 
 

MVCC的底层原理

MVCC的实现主要依赖于记录中的三个隐藏字段,undo log和Read View来实现。
 

隐藏字段

数据库表中除了我们自定义的字段外,还有数据库隐式定义的字段,如DB_TRX_ID,DB_ROLL_PTR和DB_ROW_ID等字段。

DB_TRX_ID:表示最近修改事务的id,用于记录创建这条记录或者是最后一次修改这条记录的事务的id。
DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undo log。
DB_ROW_ID:隐藏的主键,如果数据表没有主键,那么innodb会自动生成一个6字节的row_id。
 

undo log

Undo Log被称为回滚日志,表示在进行inset、delete和update操作的时候产生的方便回滚的日志。
对于undo log中的insert、delete过程,我们之后再说,为了贴近文章,这里我们先来说说update

为了便于理解,我们来举个小例子:假设有三个事务,事务1、2、3。
事务1向表中插入一条记录,如图
在这里插入图片描述
事务2修改该数据进行update。
事务2修改原本的记录之前,数据库会对该行加排它锁。
然后把原本的记录拷贝一份到undo log中,作为旧记录,既在undo log中有原本记录的拷贝副本。
当拷贝完成后,对数据进行update,并且修改隐藏字段最近修改的事务id,回滚指针指到undo log的拷贝副本中。
事务提交后,释放锁。
在这里插入图片描述

事务3也对该记录做了update。
事务3在原本的记录做update之前,数据库会对该行记录加排它锁。
然后把原本的记录拷贝一份到undo log中,作为旧记录,但是发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录undo log的最前面。
拷贝插入到表头之后,对数据进行update,并且修改隐藏字段最近修改事务的id,回滚指针指向刚刚拷贝的undo log的副本记录。
事务提交后,释放锁。

在这里插入图片描述

从这个图片可以看出,
对该记录每次更新后,都会将旧值放到一条undo log中,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个版本链,undo log的链首就是最新旧记录,链尾就是最早的旧记录。

 

Read View

为什么要有Read View的存在?
如果一条记录在undo log中有多个数据版本,并且这些又是由不同事务进行操作的,那么我们该如何选择?

Read View是事务进行快照读操作的时候产生的可读视图,在该事务执行读快照操作的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id,事务的id值是递增的。

Read View的最大作用是用来做可见性判断的,也就是说当某个事务在执行快照读的时候,对该记录创建一个Read View视图,把它当做条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最小的数据,也有可能读取的是当前行记录的undo log中某个版本的数据。

为了方便理解Read View,我们再举个例子:

 
在这里插入图片描述
 
假设我们有四个事务,事务1/2、3/4,这个时候,四个事务都已经开始了,但是事务1/2/3都没有对记录进行修改操作,事务4对记录做了修改操作,并做了提交,这个时候,DB_TRX_ID = 4

在Read View里面包含三个属性:
trx_list:Read View生成时当前系统中活跃读写事务id列表。(1/2/3,事务4已经提交了,所以就不活跃 了)
up_limit_id:记录trx_list列表中最小的事务ID。(1)
low_limit_id:Read View生成时刻系统尚未分配的下一个事务ID。(5)

Read View如下所示:

事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View视图。
trx list没有写事务2的原因是你在本事务做的修改操作,是一定能看到的,所以就没写
 在这里插入图片描述
 
那么问题就来了,事务2进行了快照读,生成了Read View如上所示,那么事务2在读数据的时候,他能读到事务四修改之后的记录吗?

所以我们这个时候就需要用到Read View的比较规则来判断事务2是否能读到事务四修改之后的记录。

具体规则如下:(用三目运算符来看)
1、首先比较DB_TRX_ID(4) < up_limit_id (1) ? 当前事务(2)能看到DB_TRX_ID所修改的记录:进入下一个判断
2、接下来比较DB_TRX_ID(4) >= low_limit_id(5) ? DB_TRX_ID(4)所在的记录在Read View(事务2)生成之后才出现的,那么对于当前的事务看到不可见:进入下一步判断
3、判断DB_TRX_ID是否在活跃事务中,如果在,则代表在Read View生成时刻,这个事务还是活跃状态,还没有commit,当前事务看不到修改的数据,如果不在,说明这个事务在Read View生成之前就已经commit,那么修改的结果是能够看见的 。

而我们在这种能否看到数据的都会涉及到隔离性。
不同的隔离级别对于Read View的比较规则是一样的,而在这个比较规则之中,唯一可以变的是Read View的值,所以RC(Read Committed)和RR(Repeatable Read)最大的区别在于生成Read View的时机不同:
如果我们是读已提交隔离级别,那么我们每次进行快照读的时候都会生成一个Read View。
如果是我们可重复读隔离级别,同一个事务中的第一个快照读才会生成一个Read View,之后快照读都是使用同一个Read View。

所以,现在我们已经知道了读已提交可重复读是如何实现的了。
而对于读未提交的事务的隔离级别来说,由于一个事务可以读到另一个未提交的事务,使用就在读取最新记录就好了。所以才会出现脏读、不可重复读和幻读。
而对于可串行化的事务隔离间级别来说,Innodb使用表锁的方式来实现(要求事务只能一个接着一个执行,不能并行执行,所以不会出现丢失修改、脏读、不可重复读和幻读)
 
 

1、读已提交是怎么实现的呢?
读已提交底层是MVCC来进行实现的,读已提交不同于可重复读隔离级别,读已提交的隔离级别,在每次进行快照读操作的时候都会生成一个Read View,而可重复读的隔离级别在同一个事务中的第一个快照读才会生成一个Read View,之后快照读都是使用同一个Read View来进行判断。
可重复读能不能解决幻读?如果可以是怎么解决的?什么情况下又会出现幻读?
对于第二个问题来说:
幻读问题的关键在于并发新增数据行,因此阻止并发数据新增可防止幻读,在串行化级别下是通过严格的加表级锁控制来解决幻读,但除非幻读影响到业务,否则不会这样做。

可重复读在范围修改的情况下一定程度上可以防止幻读,因为可重复读级别下的范围查询会对记录区间加间隙锁,间隙锁和记录锁一起构成临键锁,比如主键id为5、40的数据,可划分为三个区间(-无穷,5)、(5,40)、(40,+无穷),当查询2-30范围的数据时,(-无穷,5)、(5,40)区间会添加间隙锁,使得其他事务无法在这些区间内新增记录,但却可以在(40,+无穷)区间内新增记录,这样仍会导致幻读的发生。所以在执行范围囊括所有区间的情况下可以防止幻读。

拓展阅读:https://dev.mysql.com/doc/refman/8.0/en/innodb-next-key-locking.html
官网中指出是使用临键锁来防止幻读的。

 
 
 
  
  
  

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值