如果认为本文章对你有用,那可以为我点赞收藏加关注哦,因为创作不易,码字也不容易哦,点赞评论+关注是最好的支持。
在被面试官问面试题的时候,首先它问了Mysql的事务的隔离级别有几种?默认是哪种?他们分别解决了什么问题?
我在一顿回答“巴巴巴巴。。。。”之后,它又继续问题了我:
1、读已提交是怎么实现的呢?
2、可重复读能不能解决幻读?如果可以是怎么解决的?什么情况下又会出现幻读?
我顿时不知道怎么回答,从这里,其实过后我才知道它已经把我从MySQL事务的隔离级别引到了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被称为回滚日志,表示在进行inset、delete和update操作的时候阐释的方便回滚的日志。
当进行inser操作的时候,产生的undo log只在事务回滚的时候需要,并且在事务提交之后可以被丢弃。
当进行update和delete操作的时候,产生的undo log不仅仅在事务回滚的时候需要,在快照读的时候也需要,所以不能随便删除,只有在事务回滚或者快照读不涉及该日志是,对应的日志才会被purge线程统一清除。
为了便于理解,我们来举个小例子:假设有三个事务,事务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的存在?
如果一条记录在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使用表锁的方式来实现(要求事务只能一个接着一个执行,不能并行执行,所以不会出现丢失修改、脏读、不可重复读和幻读)
可重复读能不能解决幻读?如果可以是怎么解决的?什么情况下又会出现幻读?
对于第二个问题来说:
幻读问题的关键在于并发新增数据行,因此阻止并发数据新增可防止幻读,在串行化级别下是通过严格的加表级锁控制来解决幻读,但除非幻读影响到业务,否则不会这样做。
可重复读在范围修改的情况下一定程度上可以防止幻读,因为可重复读级别下的范围查询会对记录区间加间隙锁,间隙锁和记录锁一起构成临键锁,比如主键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
官网中指出是使用临键锁来防止幻读的。