主页 > 官网最新版imtoken钱包 > RR 级别和 RC 级别的 Read View 生成时间
RR 级别和 RC 级别的 Read View 生成时间
文章目录
MySQL-InnoDB-MVCC 多版本并发控制
InnoDB 是一个多版本的存储引擎。它保留有关已更改行的旧版本的信息,以支持并发和回滚等事务功能。
参考:InnoDB 多版本
也就是说InnoDB是一个多版本的存储引擎,它通过保留记录的旧版本信息来支持事务功能,例如并发和回滚。所以这里所说的多版本就是我们所说的MVCC(Multi-Version Concurrency Control),多版本并发控制。
为什么需要 MVCC?
要学东西,先问为什么,再学原理。那么为什么我们需要MVCC呢?或者说MVCC给我们带来了什么好处?
在 MySQL InnoDB 中 MVCC 的实现主要是为了提高数据库的并发性能,更好的处理读写冲突。这里有什么更好的表达方式?传统上,我们在解决并发问题时,最常用的方法是加锁(悲观加锁),而MVCC使用更好的方法来实现无锁定、无阻塞的并发读取,即使发生读写冲突。
我们知道读写不会有并发问题,如果出现读写冲突,就会出现我们之前博文提到的事务隔离问题,可能会遇到脏读和不可重复读。幻读。并且MVCC可以实现读操作不阻塞写操作,写操作不阻塞读操作,提高了数据库的并发读写性能。它还可以解决一些事务隔离问题,例如脏读、不可重复读和幻读。 MVCC无法彻底解决(++Innodb在RR级别使用MVCC解决snapshot reads中的幻读问题,current reads中出现幻读问题,需要使用GAP lock,即gap lock++来解决)。对于写-写冲突,MVCC无法解决,写-写冲突会有更新丢失的问题,比如第一种更新丢失,第二种更新丢失。
关于丢失更新的问题,这里就不过多解释了。可以看到:什么是脏读、不可重复读、幻读?
所以总的来说,MVCC 是一种通过悲观锁来解决读写冲突的解决方案,这还不够好。所以MVCC解决了读写冲突,写冲突通过悲观锁或乐观锁解决。
什么是当前读取和快照读取?
前面提到过MVCC解决读写冲突,实现非锁定、非阻塞并发读取。 MySQL 中的所有读取是否都以解锁方式使用 MVCC?其实这里的读指的是快照读。
那我们需要谈谈什么是快照阅读,什么是当前阅读。
快照读取
我们通常直接使用的非加锁select就是快照读,也就是不加锁的非阻塞读;快照读取的前提是隔离级别不是字符串行级别,串行级别的快照读取会退化为当前读取;之所以会发生快照读,是出于提高并发性能的考虑,而快照读的实现是基于MVCC的。快照读取不一定会读取到最新版本的数据,可能是之前的历史版本。
当前阅读
共享模式下选择锁(共享锁)、选择更新等操作;更新;插入; delete(排他锁)都是当前读,为什么叫当前读?也就是说持币快照是什么意思,它读取最新版本的记录。读取时,必须保证其他并发事务不能修改当前记录,并锁定读取的记录。
MVCC的实现原理
MVCC的目的是多版本并发控制。数据库中的实现是为了解决读写冲突。其实现原理主要依赖于记录中的三个隐含字段,undo log和Read View。
三个隐藏字段
InnoDB 在内部为每条记录添加了三个隐藏字段。
一个 6 字节的 DB_TRX_ID 字段表示插入或更新行的最后一个事务的事务标识符。此外,删除在内部被视为更新,其中设置了行中的特殊位以将其标记为已删除。
一个 7 字节的 DB_ROLL_PTR 字段称为滚动指针。滚动指针指向写入回滚段的撤消日志记录。如果行已更新,撤消日志记录包含在更新之前重建行内容所需的信息。
一个 6 字节的 DB_ROW_ID 字段包含一个行 ID,该行 ID 随着新行的插入而单调增加。如果 InnoDB 自动生成聚集索引,则索引包含行 ID 值。否则,DB_ROW_ID 列不会出现在任何索引中。
首先解释一下这个DB_TRX_ID,我们都知道InnoDB是支持事务的,innodb中的每一个东西都会分配一个自增的ID作为事务的唯一标识,也就是事务ID ,而 DB_TRX_ID 用于存储创建或最后修改此记录的事务 ID。同时官网也提到了,删除在内部被视为更新,在该行中设置了一个特殊位,将其标记为已删除。这说明MySQL中的删除并不是真正的删除,而是实际上有一个delete flag隐藏字段,即更新或删除记录。这里的删除并不意味着立即物理删除,而是将这条记录的删除标志改为true。最终的删除操作由清除线程完成
MySQL删除官网解释是这样的:在InnoDB多版本方案中,使用SQL语句删除一行并不会立即从数据库中物理删除。 InnoDB 仅在丢弃为删除而写入的更新撤消日志记录时,才会物理删除相应的行及其索引记录。这种删除操作称为清除,速度很快,通常与执行删除的 SQL 语句的时间顺序相同。
DB_ROLL_PTR:回滚指针,指向这条记录的上一版本 DB_ROW_ID:隐式自增ID(隐藏的主键),如果数据表没有主键,InnoDB会自动生成一个聚集索引的Undo Log使用 DB_ROW_ID
撤消日志有两个功能:提供回滚和多行版本控制(MVCC)。
当数据被修改时,不仅会记录redo,还会记录对应的undo。如果事务失败或者由于某种原因被回滚,可以使用undo来回滚。
可以简单的认为,当一条记录被删除时,undo log中会记录一条对应的insert记录,反之,一条记录被更新时,也会记录一条对应的对面更新记录。
执行回滚时,可以从undo log中的逻辑记录中读取相应的内容并回滚。应用于行版本控制时持币快照是什么意思,也是通过undo log实现的:当某行读被其他事务锁定时,可以从undo log中分析该行之前的数据,从而提供Row版本信息,让用户实现非锁定一致性读取。
旧版本的数据存储在撤消日志中。当一个旧事务需要读取数据时,为了读取旧版本的数据,需要沿着undo链找到满足其可见性的记录。
所以undo log分为以下两类:
插入撤消日志
大部分的数据变更操作包括INSERT/DELETE/UPDATE,其中INSERT操作在事务提交前只有当前事务可见,并且只有事务回滚时才需要Insert Undo log,所以生成的Undo log事务提交后可以直接删除(因为对刚插入的数据没有可见性要求)。
更新撤消日志
对于 UPDATE/DELETE,需要维护多版本信息。在 InnoDB 中,UPDATE 和 DELETE 操作产生的 Undo 日志归为一类,即 update undo log 。事务回滚时需要更新undo log,读取快照时也需要,所以需要维护多个版本信息。只有当日志不参与快照读取和事务回滚时,相应的日志才会被清除线程统一删除。 purge 线程会清理 undo log 的历史版本,以及带有 del 标志的记录。
在存储结构上也分为insert undo log和update undo log
用于插入撤消日志。存储记录中没有回滚指针DB_ROLL_PTR(没有旧值,插入前没有)
用于更新撤消日志。有一个带有旧值的回滚指针 DB_ROLL_PTR。
undo log 是分段记录的,每个 undo 操作在记录时占用一个 undo log 分段。 innodb 存储引擎以分段的方式管理撤消。回滚段称为回滚段,每个回滚段中有1024个undo log段。所以真正对MVCC有用的是更新undo log,它是依靠回滚段中undo log段形成的旧记录链来实现的
记录链图
假设我们现在有一个 person 表有两个字段,一个 name 和一个 age。
现在我们有一个事务1插入一条新记录,该记录名为Kevin,年龄为18,然后因为没有主键,所以会有一个隐含的主键1,我们这里假设的事务ID是1,然后回滚指针为空
现在事务 2 修改了记录名,改成了 Jerry。
所以一旦记录发生变化,旧数据的最新副本会在链头,旧数据最旧的副本会在链尾,这样就形成了一连串的记录。当然,这些数据副本不会永远保留。如前所述,将有一个专门的纯线程来删除这些不再需要的数据副本。
所以MVCC通过这样一条记录链找到旧的数据副本。
阅读视图阅读视图
Read View 是事务执行快照读取操作时产生的读取视图(Read View)。在事务执行快照读取的时刻,会生成数据库系统的当前快照,记录并维护系统当前活动事务的ID(每个事务打开时,都会分配一个ID,这个ID是递增的,所以最近的交易,ID值越大)
Read View主要用于可见性判断,即当我们对事务执行快照读取时(执行正常的select操作时),会为记录创建一个Read View,然后使用这个Read View读取视图作为判断条件,判断当前事务可以看到哪个版本的数据,可能是当前最新的数据,也可能是该行记录的undo log中数据的某个版本。
Read View 遵循可见性算法,主要是取出当前事务ID,与系统中其他活跃事务的ID进行比较(活跃事务是尚未提交的事务)(由Read View维护),如果DB_TRX_ID与Read View的属性比较,不满足可见性,则使用DB_ROLL_PTR回滚指针取出Undo Log中的DB_TRX_ID进行比较,即遍历链表的DB_TRX_ID(从链头到链尾,即从最近的修改开始检查)直到找到满足特定条件的DB_TRX_ID,那么DB_TRX_ID所在的旧记录就是最新旧版本可以被当前事务看到
那么我们将在下面讨论这个条件。什么?
从Mysql的源码来看,Read View简单理解为具有三个全局属性
接下来就是判断逻辑了。
但是当我们执行选择操作时,会生成一个Read View。这时,我们需要选择对当前事务可见的数据。这时候,我们就需要做出一个合乎逻辑的判断了。首先,我们会找到这条记录的记录链,也就是我们的undo log,然后沿着undo log链找到满足其可见性的记录。那么满足以下条件。我们先从链头获取一个DB_TRX_ID,然后开始比较DB_TRX_ID。
如果上面的比较都是不可见的,那么通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID,进行上面的比较,直到找到可见的记录
参考来自:CSDN博主“SnailMann”的原创文章。原文链接:
MVCC的整体流程
现在我们了解了隐藏字段、撤消日志和读取视图的概念,我们可以开始看一下 MVCC 实现的整体流程了。
以下有 4 笔交易。在事务2中,对一行数据进行快照读取,数据库为这行数据生成一个Read View。事务 4 在快照读取之前已被修改并提交,事务 1 和 2 都是活动事务。
事务 1 事务 2 事务 3 事务 4
交易开始
交易开始
交易开始
交易开始
…
…
…
修改并提交
进行中
快照读取
进行中
…
…
…
根据我们上面的描述,在进行快照读取时,会维护此时所有活跃事务的ID列表。这个列表是 trx_list。这个trx_list中有活跃的事务1和3,所以trx_list:[1,3]
ReadView不仅维护了事务2执行快照读取时处于活动状态的事务ID列表,还有两个属性up_limit_id(trx_list列表中事务ID最小的ID),low_limit_id(快照读取时的系统)下一个尚未分配的事务ID,即到目前为止已出现的事务ID的最大值+ 1)
所以本例中up_limit_id为1,low_limit_id为4 + 1 = 5,trx_list集合的值为[1, 3],Read View如下图
在事务2执行快照读取之前,只有事务4执行修改和提交,所以快照读取时刻的undo日志如下:
现在我们使用Read View的可见性逻辑来判断记录链上哪条记录是可见的。
首先,我们同一个事务2在执行快照读取的时候会产生Read View。 Read View的三个属性是trx_list: [1,3], up_limit_id: 1, low_limit_id: 5。
之后,我们将从记录链的开头开始搜索,即从记录链的最新记录开始。
最新记录的DB_TRX_ID是4.用4和Read View的up_limit_id比较,4不小于up_limit_id(1),继续判断。写下来判断4是否为大于等于low_limit_id(5),同样不满足条件。最终判断4是否在trx_list([1,3])中,发现4不在活跃事务列表中,满足可见性条件. 所以事务4修改后提交的最新结果对事务2的快照读取是可见的,因此事务2可以读取到最新记录是事务4提交的版本,事务4提交的版本也是最新版本从全球的角度来看。
接下来,我们来看一个完整的流程图。流程图来自:
参考自:CSDN博主“SnailMann”原创文章。原文链接:
RR 级别和 RC 级别的 Read View 生成时间
我们前面提到,Read View 是事务执行快照读取操作(Read View)时产生的读取视图。 ),在事务执行快照读取的时刻,会生成数据库系统的当前快照。这个描述不够准确。
因为在RR级别和RC级别,Read View的生成时序是不同的。因此会出现不可重复读和可重复两种情况。
首先,RC级别是不可重复读(read-committed)。在这个级别,Read View的生成时机是每执行一次select操作就生成一个Read View,也就是执行一次快照读取。所以每个快照都会刷新,所以自然会读取到不同的结果。如果我们通过表格显示会更明显。
顺序事务 A 事务 B
1
开启交易
开启交易
2
快照读取(无效),查询量为500
选择快照读取,查询量为500
3
更新量为400
p>
4
提交事务
5
选择快照读取,查询量为400
6
当前共享模式下select lock的读取量为400
这里,事务 B 在序列 2 读取的数量是 500,但是在序列 5 读取的数量变成了 400,所以这里是不可重复读取。
为什么会这样?这是因为在RC层面,在snapshot读取的时候,也就是在sequence 2和sequence 5的时刻,分别生成了Read Views,但是在sequence 2的时候Transaction A仍然是Read View中的活跃事务此刻,但是事务A在序列5的时刻并不是一个活跃事务,因为它已经提交了,所以生成的Read View不一样,这会导致为什么它可以在序列5中被读取。获取到的数据事务 A 已提交。
同样的例子让我们看看它在 RR 级别的样子。
首先RR级别是repeatable-read,这是Mysql默认的隔离级别,可以解决不可重复读。那么他是怎么解决的呢?事实上,Read View 的创建时机与 RC 不同。在RR级别,Read View的创建时机只有在事务第一次执行select snapshot read时,才会生成Read View。读取快照时,不会生成新的Read View,而是使用之前生成的Read View来判断可见性。
接下来,我们来看例子
顺序事务 A 事务 B
1
开启交易
开启交易
2
快照读取(无影响),查询量为500
选择快照读取,查询量为500
3
更新量为400
4
提交事务
5
选择快照读取,查询量为500
6
共享模式下选择锁定当前阅读量为400
对结果有疑问的同学可以去Mysql自己试试结果。我自己的验证结果是一致的。
所以这里在RR级别,Read View只会在事务B第一次执行快照读取时生成,即在序列2的时刻生成Read View,然后在序列5的时刻。在执行快照读取时不会生成Read View,所以仍然使用序列2时生成的Read View,所以序列5时读取的快照仍然是相同的Read View ,所以读取快照结果还是500。但是在共享模式下使用select lock时,读取结果必须是400,因为读取的是最新的数据。
同样是在RR层面,我们来看一个例子:
顺序交易A交易B
1
开启交易
开启交易
2
快照读取(无效),查询量为500
3
更新量为400
4
提交事务
5
选择快照读取,查询量为400
6
当前共享模式下select lock的读取量为400
这个和上面例子的区别在于第一次snapshot读取的时机不同,之前第一次snapshot读取的时机是在序列2的时刻,而现在第一次snapshot读取的时刻是在序列 5 的时刻。我们知道,在 RR 级别,事务中快照读取的结果非常依赖于事务中第一次发生快照读取的位置。即事务中第一次发生快照读取的地方是非常关键的,它有能力判断事务后续快照读取的结果。所以这里第一次读取快照的时刻是在序列5的时刻,所以这次Read View是在序列5的时刻生成的,因为此时事务A已经完成了修改操作并提交了。因此,此时事务B的快照读取也可以读取到最新的数据。
所以从 RC 和 RR 级别的 Read View 生成时序可以看出,Read View 的设计是相当巧妙的。 RR 和 RC 级别都可以共享完全相同的读取视图逻辑,即使仅来自读取视图。从这个角度来看,RR级别比RC级别消耗更少的系统资源。难怪mysql的默认级别是RR。
总结
是Read View的生成时序不同,导致RC,RR级别下snapshot read的结果不同
简而言之,在RC隔离级别下,每次快照读取都会生成并获取最新的Read View; RR隔离级别下,是同一个事务,只会为第一个snapshot读入创建Read View,后续的snapshot读获取相同的Read View。
本文小结参考
InnoDB 多版本
MySQL的删除操作其实是假的删除
我很困惑!女朋友突然问我MVCC的实现原理
Mysql中的MVCC机制
【MySQL笔记】正确理解MySQL的MVCC及其实现原理
详细分析MySQL的事务日志(重做日志和撤消日志)
MySQL详细解读undo log:insert undo,update undo
两种撤销日志记录格式
MySQL 在 RC 隔离级别下如何实现非阻塞读取?