由浅入深全面理解MVCC-2

目录

1、什么是MVCC

2、什么是InnoDB存储引擎下的当前读和快照读

3、当前读、快照读与MVCC的关系

4、MVCC能够解决什么问题

4.1 数据库中的三种并发场景

4.2 MVCC能够解决的问题:读-写冲突

4.2.1 悲观并发控制、乐观并发控制 与MVCC的关系

4.2.2 MVCC能够解决什么问题

5、MVCC的实现原理

5.1 三个隐式字段

5.2 undo log 回滚日志

5.2.1 undo log 生成举例

5.3 ReadView 读视图

5.4 MVCC的整体流程

5.5 MVCC的相关问题

5.5.1 RR是如何在RC的基础上解决不可重复读的?

5.5.2 RC、RR级别下的InnoDB快照读有什么不同?

99、参考

1、什么是MVCC

MVCC,全称是 Multi-Version Concurrency Control,多版本并发控制。MVCC是一种并发控制的方法,一般情况下,在数据库管理系统中实现对数据库的并发访问。【MVCC解释-来自百度】

MVCC 在MySQL InnoDB 存储引擎中的实现,主要是为了提高数据库的并发性能,用更好的方式来处理【读-写冲突】:即 在并发读-写数据库时,可以做到在读操作时不用阻塞写操作,在写操作时也不用阻塞读操作,提高了数据库并发读写的性能。

2、什么是InnoDB存储引擎下的当前读和快照读

(1)当前读:像 【 select ... lock in share mode (共享锁)、select ... for update (排它锁)、update / insert / delete (排它锁) 】这些SQL操作都是一种当前读。为什么叫当前读?就是因为它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。当前读,实际上是一种加锁的操作,是悲观锁的实现。

(2)快照读:像【 不显示加锁的 select ... 操作 】就是快照读,即不加锁的非阻塞读。快照读的前提 是事务的隔离级别不是串行隔离级别,串行隔离级别下的快照读会退化成当前读。之所以会出现快照读,是基于提高数据库并发性能的考虑,快照读的实现是基于多版本并发控制 即 MVCC,可以认为 MVCC 是行锁的一个变种,但是,它在很多情况下 避免了加锁操作,降低了开销。既然是基于多版本并发控制,那么 快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。说白了,快照读就是MVCC思想在MySQL中的具体的非阻塞读功能的实现,整个MVCC多版本并发控制的目的 就是为了实现【读-写冲突不加锁】,提高数据库的并发读写性能,而这个读指的就是快照读。

3、当前读、快照读与MVCC的关系

(1)准确的说,MVCC 多版本并发控制指的是:“ 维持一个数据的多个版本,使得【读-写操作】没有冲突 ” 的这么一个概念。仅仅是一个概念而已。

(2)而在MySQL中,要实现这么一个MVCC的概念,就需要MySQL提供具体的逻辑功能去实现它,而 快照读 就是MySQL为我们实现MVCC概念模型的一个具体的非阻塞读功能的实现。当前读 则是MySQL中悲观锁的一个具体功能实现。

(3)MVCC模型在MySQL中的具体实现则是由【 3个隐式字段、undo 回滚日志 、ReadView 读视图】去完成的,具体可以看下面的MVCC实现原理。

4、MVCC能够解决什么问题

4.1 数据库中的三种并发场景

在数据库中,并发场景主要有三种,分别是:

(1)读-读 并发:不存在任何问题,也不需要并发控制。

(2)读-写 并发:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读 问题。

(3)写-写 并发:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失、第二类更新丢失。

在MySQL中,如果没有做好并发控制,就有可能会出现上述问题。

4.2 MVCC能够解决的问题:读-写冲突

4.2.1 悲观并发控制、乐观并发控制 与MVCC的关系

(1)悲观并发控制(PCC),是一种用来解决【读-写冲突】和【写-写冲突】的加锁并发控制,为每个操作都加锁,同一时间下,只有获得该锁的事务才能有权利对该数据进行操作,没有获得锁的事务只能等待其他事务释放锁。所以,可以解决 脏读、幻读、不可重复读、第一类更新丢失、第二类更新丢失的问题。

(2)乐观并发控制(OCC),是一种用来解决【写-写冲突】的无锁并发控制,它认为事务之间的数据争用没有那么多,所以先进行修改,在提交事务前,检查一下事务开始后,有没有数据修改被提交,如果没有就提交,如果有就放弃并重试。乐观并发控制类似于自旋锁。乐观并发控制适用于低数据争用、写冲突比较少的场景。无法解决脏读、幻读、不可重复读,但是可以解决更新丢失类问题。

(3)多版本并发控制(MVCC),是一种用来解决【读-写冲突】的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改都保存一个版本,版本与事务的时间戳相关联,读操作只读该事务开始前的数据库的快照。 这样,在读操作时就不用阻塞写操作,写操作也不用阻塞读操作。不仅可以提高并发性能,还可以解决脏读、幻读、不可重复读等事务问题,更新丢失类问题除外。

4.2.2 MVCC能够解决什么问题

(1)MVCC 的含义:MVCC,也就是 多版本并发控制,是一种用来 解决【读-写冲突】的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务的时间戳相关联,读操作只能够读取该事务开始前的数据库的快照。

(2)所以,MVCC可以为数据库解决以下问题:

(2.1)在并发读-写数据库时,可以做到在读操作时不用阻塞写操作,在写操作时也不用阻塞读操作,提高了数据库并发读写的性能。

(2.2)同时,MVCC 还可以解决 脏读、不可重复读、幻读 等事务隔离问题,但不能解决更新丢失类问题。

(3)总的来说:MVCC的出现就是 数据库不满 用悲观锁去解决读-写冲突问题 因性能不高而提出的一种解决方案,所以在数据库中,我们可以形成两个组合:

(1)MVCC + 悲观锁:MVCC解决 读-写冲突,悲观锁解决 写-写冲突。
(2)MVCC + 乐观锁:MVCC解决 读-写冲突,乐观锁解决 写-写冲突。

这2种组合的方式,就可以最大程度的提高数据库并发性能,并解决【读-写冲突】和【写-写冲突】导致的问题。【参考:乐观锁和MVCC的关系】

5、MVCC的实现原理

MVCC的含义就是多版本并发控制,在数据库中的实现,其目的就是为了解决数据库中的【读-写冲突】问题。它的实现原理主要是依赖记录中的【 三个隐式字段、undo 回滚日志 、ReadView 读视图】 来实现的。所以,我们逐一看看这个三个关键的概念。

5.1 三个隐式字段

在MySQL中,每行记录除了我们自己定义的字段之外,还有数据库隐式定义的一些字段:DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID 等字段。

(1)DB_TRX_ID:数据行事务版本号,大小为6byte,记录 最近修改事务ID(这里的修改包含:insert / update ),记录创建这条记录 / 最后一次修改这条记录的事务ID。

(2)DB_ROLL_PTR:回滚指针,大小为7byte,记录 回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)。

(3)DB_ROW_ID:行记录隐式ID,大小为6byte,每行记录都存在着一个隐含的自增主键ID(隐藏自增主键,并不是我们在表中创建的自增主键ID)。如果我们在创建数据表时没有创建主键ID,那么,InnoDB存储引擎会自动的以 DB_ROW_ID 产生一个聚簇索引来作为自增主键ID。

解释:如上图所示,DB_ROW_ID 是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID 是当前操作该记录的事务ID,DB_ROLL_PTR 是一个回滚指针,用于配合undo日志,指向该条记录的上一个旧版本。

5.2 undo log 回滚日志

undo log,它是MySQL中的回滚日志,主要分为两种:

(1)insert undo log:代表事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交之后可以被立即丢弃。

(2)update undo log:代表事务在进行 update 或者 delete 时产生的 undo log,不仅在事务回滚时需要,在快照读时也需要,所以不能随便删除。

另外,对purge线程做一些解释:

从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新操作 update 或者 删除操作 delete 都只是设置一下老记录的 deleted_bit 为 true 而已,并不是真正将过时的记录删除。

为了节省磁盘空间,InnoDB有专门的purge线程来清理 deleted_bit 为true的记录。

为了不影响MVCC的正常工作,purge线程自己也维护了一个 ReadView(这个ReadView相当于系统中最老活跃事务的ReadView):如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID相对于purge线程的ReadView可见,那么,这条记录就一定是可以被安全清除的。

5.2.1 undo log 生成举例

对MVCC有帮助的实质是 update undo log ,undo log 实际上就是存在 rollback segment 中的旧记录链,它生成的执行流程如下:

【一】比如,有一个事务在person表中插入了一条新记录,记录如下:name为Jerry、age为24岁,隐式主键是1,事务ID回滚指针,我们假设为NULL:

【二】 现在,来了一个事务1 对该记录的name做出了修改,改为Tom:

(1)首先,在事务1 修改该行记录时,数据库会先对该行加排他锁;

(2)然后,把该行数据拷贝到 undo log 中,作为旧记录,即在 undo log 中有当前行的拷贝副本;

(3)拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务的事务ID,为1,回滚指针 为指向拷贝到undo log的副本记录,即表示我的上一个版本就是它。

(4)事务1 提交后,释放该行的排他锁。

【三】 这时,又来了一个事务2,它修改person表的同一个记录,将age修改为30岁:

(1)首先,在事务2 修改该行数据时,数据库也先为该行加排它锁;

(2)然后,把该行数据拷贝到 undo log 中,作为旧记录,发现该行记录已经有 undo log 了,那么,最新的旧数据就作为链表的表头,插在该行记录的 undo log 最前面。

(3)修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务的事务ID,为2,回滚指针 为指向刚刚拷贝到 undo log 的副本记录,即表示我的上一个版本就是它。

(4)事务2 提交之后,释放该行的排他锁。

从上面我们可以看出:不同事务或者相同事务的对同一条记录的修改,会导致该条记录的 undo log 成为一条记录版本的链表,undo log 的链首就是最新的旧记录,undo log 的链尾就是最早的旧记录。当然,就像之前说的该 undo log 的节点可能会被purge线程清除掉,像图中的第一条 insert undo log,其实在插入数据的事务提交之后可能就被删除了,不过这里为了演示,所以还放在这里。

5.3 ReadView 读视图

什么是 ReadView?说白了,ReadView 就是事务在进行快照读操作的时候生成的一个读视图(ReadView)。在该事务执行快照读的那一刻,会为当前的数据库系统生成一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID,这个ID是单向递增的,所以最新的事务,ID值越大)。

所以,我们知道 ReadView 主要是用来做数据可见性判断的。即 当某个事务执行快照读的时候,对该记录创建一个ReadView读视图,把它当作条件用来判断当前事务能够看到该条记录的哪个版本,既可能是”该条记录“当前最新的数据,也有可能是”该条记录“的 undo log 里面的某个旧版本的数据。

ReadView 遵循一个可见性算法,主要是将要被修改的数据的最新记录中的 DB_TRX_ID(当前事务ID)取出来,与系统当前其他活跃事务的ID列表去对比(由ReadView维护)。如果 DB_TRX_ID 跟 ReadView 的属性做了某些比较,不符合可见性,那就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中上一个版本中的 DB_TRX_ID 再比较,即遍历版本链的 DB_TRX_ID( 从链尾到链首,即从最近的一次修改查起),直到找到满足特定条件的 DB_TRX_ID,那么,这个 DB_TRX_ID 所在的旧记录就是当前事务所能看见的最新的老版本。

那么这个判断条件是什么呢?

这里盗窃 【呵呵一笑百媚生】 的一张源码图,如上所示:它是MySQL判断可见性的一段源码,即changes_visible方法,该方法展示了拿DB_TRX_ID去跟ReadView某些属性进行怎样的比较。

在解释之前,先简化一下ReadView,可以把ReadView简单的理解成有三个全局属性:

(1)trx_list:一个ID列表,用来维护 ReadView 生成时刻系统正处于活跃状态的事务ID列表。

(2)up_limit_id:记录 trx_list 列表中事务ID最小的那个ID。

(3)low_limit_id:ReadView 生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的最大事务ID的值+1。

DB_TRX_ID (当前事务ID)跟 ReadView 这三个全局属性进行比较的流程是:

(步骤 1)首先,比较 DB_TRX_ID 是否小于 up_limit_id:如果小于,则当前事务仅能看到 DB_TRX_ID 所在的记录;如果大于等于,则进入下一个判断。

(步骤 2)接下来判断 DB_TRX_ID 是否大于等于 low_limit_id:如果大于等于,则代表 DB_TRX_ID 所在的记录在 ReadView 生成之后才出现,对当前事务肯定不可见;如果小于,则进入下一个判断。

(步骤 3)DB_TRX_ID 是否在活跃事务ID列表之中,trx_list.contains(DB_TRX_ID):如果在,则代表 我的ReadView 生成时刻,你这个事务还在活跃,还没有 commit,你修改的数据 我当前事务当然看不见;如果不在,则说明你这个事务在 ReadView 生成之前就已经 commit 了,你修改的结果我当前事务能看见。

5.4 MVCC的整体流程

在了解了 【 三个隐式字段、undo 回滚日志 、ReadView 读视图】的概念之后,就可以来看看MVCC实现的整体流程了。MVCC的整体流程是怎样的?可以模拟一下。

(1)当 事务2 对某行数据执行了快照读数据库为该行数据生成一个ReadView读视图,假设当前事务的事务ID为2。此时,还有 事务1 和 事务3 处在活跃中,事务4 在 事务2 快照读的前一刻提交更新了,所以 事务2 中 该数据行的ReadView读视图 记录了当前系统活跃的 事务1 和 事务3 的ID,维护在一个列表上,假设称为 trx_list=[1,3]。

(2)ReadView 不仅仅会通过一个列表 trx_list 来记录 事务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],ReadView 如下图:

(3)在例子中,只有 事务4 修改过该行记录,并在 事务2 执行快照读之前 就提交了事务,所以,当前该行当前数据的 undo log 如下图所示。

事务2 在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID 去跟 up_limit_id、low_limit_id、活跃事务ID列表(trx_list)进行比较,判断 当前事务2 能看到该记录的版本是哪个。

(4)所以,在该行记录的版本链中,首先拿该记录 DB_TRX_ID 字段记录的 事务ID 4 去跟 ReadView 的 up_limit_id 比较,看 4 是否小于 up_limit_id (1),不符合条件;所以继续判断 4 是否大于等于 low_limit_id (5),也不符合条件;所以最后判断 4 是否处于 trx_list 中的活跃事务ID列表中,发现 事务ID=4 的事务 也不在当前活跃事务ID列表中。所以,根据可见性条件,事务4 修改后提交的最新结果对 事务2 的快照读 可见,所以,事务2 能读到的最新数据记录是 事务4 所提交的版本,而 事务4 提交的版本也是全局角度上最新的版本。

也正是 ReadView 生成时机的不同,从而造成 RC、RR 隔离级别下 快照读的结果的不同。

5.5 MVCC的相关问题

5.5.1 RR是如何在RC的基础上解决不可重复读的?

当前读和快照读在 RR 隔离级别下的区别,如下的表1和表2 分别做演示。

解释:在表1的SQL顺序下,事务B 在 事务A  提交修改后的快照读是旧版本数据 500,而当前读是实时新数据 400。

解释:在表2的SQL顺序下,事务B 在事务A 提交后的快照读和当前读都是实时的新数据 400,这是为什么呢?

表2与表1的唯一区别:仅仅是表1的 事务B 在 事务A 修改金额前快照读过一次金额数据,而表2的 事务B 在 事务A 修改金额前没有进行过快照读。

所以,我们知道 事务中快照读的结果是非常依赖该事务首次出现快照读的位置的,即 某个事务中首次出现快照读的位置非常关键,它有能力决定该事务后续快照读的结果。

我们这里测试的是 更新update,同时存在 删除delete和更新update 也是一样的,如果 事务B 的快照读是在 事务A 的操作之后进行的,那么,事务B 的快照读也是能读取到最新的数据的。

5.5.2 RC、RR级别下的InnoDB快照读有什么不同?

正是 ReadView 生成时机的不同,从而造成 RC、RR 隔离级别下 快照读 结果的不同。

(1)在RR隔离级别下,某个事务中对某条记录的第一次快照读会创建一个快照以及ReadView,它会将当前系统中处于活跃的其他事务ID记录下来,此后,在调用快照读的时候,使用的仍然是同一个ReadView。所以,只要当前事务在其他事务提交更新之前使用过快照读,那么,当前事务中之后的快照读使用的都是同一个ReadView,所以,其他事务的之后的修改 对当前事务 均不可见。

即:RR隔离级别 解决了不可重复读问题。

(2)在RC隔离级别下,在某个事务中,每次快照读都会生成一个新的快照和ReadView,这就是我们在RC隔离级别下的事务中可以看到其他事务提交的更新的原因。

即:RC隔离级别 不能解决不可重复读问题。

总结:

在RC隔离级别下,每个快照读都会重新创建并获取最新的ReadView。

在RR隔离级别下,则是同一个事务中的第一个快照读才会创建ReadView,之后的快照读使用的都是同一个ReadView。

99、参考

(1)由浅入深全面理解MVCC:https://blog.csdn.net/cmm0401/article/details/115867006

(2)由浅入深全面分析乐观锁、悲观锁、MVCC:https://blog.csdn.net/cmm0401/article/details/115816459

(3)数据库事务:https://blog.csdn.net/cmm0401/article/details/115655095

(4)https://www.zhihu.com/question/66320138/answer/241418502

(5)http://m.imooc.com/article/details?article_id=17290

(6)https://www.cnblogs.com/nick-huang/p/6653996.html

(7)https://blog.csdn.net/SnailMann/article/details/88299127

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值