这可能是最直白的MySQL MVCC

什么是MVCC?

百度百科:

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

  1. MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
  2. 我们都知道并发访问数据库造成的四种问题(脏写(修改丢失)、脏读、不可重复读、幻读),MVCC就是在尽量减少锁使用的情况下高效避免这些问题

那你知道数据库的并发一致性问题嘛?

  • 脏读:事务A更新了数据,但还没有提交,这时事务B读取到事务A更新后的数据,然后事务A回滚了,事务B读取到的数据就成为脏数据了。
  • 不可重复读:事务A对数据进行多次读取,事务B在事务A多次读取的过程中执行了更新操作并提交了,导致事务A多次读取到的数据并不一致。
  • 幻读:事务A在读取数据后,事务B向事务A读取的数据中插入了几条数据,事务A再次读取数据时发现多了几条数据,和之前读取的数据不一致。
  • 丢失修改:事务A和事务B都对同一个数据进行修改,事务A先修改,事务B随后修改,事务B的修改
    覆盖了事务A的修改。

不可重复度和幻读看起来比较像,它们主要的区别是:在不可重复读中,发现数据不一致主要是数据被更新了。在幻读中,发现数据不一致主要是数据增多或者减少了

数据库的隔离级别有哪些?

  • 读未提交:一个事务在提交前,它的修改对其他事务也是可见的。
  • 读已提交(RC):一个事务提交之后,它的修改才能被其他事务看到。
  • 可重复读(RR):在同一个事务中多次读取到的数据是一致的。
  • 串行化:需要加锁实现,会强制事务串行执行。
隔离级别脏读不可重复读幻读
读未提交
读已提交×
可重复读××
串行化×××

MySQL的默认隔离级别是可重复读。

在了解MVCC时应该先了解当前读和快照读?

  • 当前读:读取的是数据库的最新版本,并且在读取时要保证其他事务不会修该当前记录,所以会对读取的记录加锁。
    (像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读)
  • 快照读:快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
    (像不加锁的 select 操作就是快照读)

假设我们开启了以下的事务:
在这里插入图片描述
有些同学可能看不懂这个图是什么意思,我来讲一下

事务A:对stu表下的name字段做了更新并且提交
事务B:在事务A的基础上做了更新,还未提交,事务D做了select1
事务C:在事务B的基础上做了更新,还未提交,事务D做了select2
事务D:在不同的时间,做了两次查询,第一次是在事务A已经提交,事务B和C 还未提交的时候做了select1,第二次是在事务A和事务B都已经提交,事务C还未提交的时候做了select2

在不同隔离级别下我们查询出来的结果会是什么样子的那?

我们直接给出结果,后面再去讲为什么!!

RR级别:Select1=张三 Select2=张三
RC级别:Select1=张三 Select2=张小三

由此我们可以看到 :RC级别下出现了“不可重复读”

这我们就要讲以下MVCC的实现原理了


MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。所以我们先来看看这个三个 point 的概念

隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID 等字段

  • DB_TRX_ID
    6 byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID
  • DB_ROLL_PTR
    7 byte,回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)
  • DB_ROW_ID
    6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引
  • 实际还有一个删除 flag 隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除 flag 变了

undo log

undo log在事务提交之前就产生,当事务提交的时候,不会删除undo log,因为可能需要rollback操作,要执行回滚(rollback)操作时,从缓存中读取数据。InnoDB会将事务对应的日志保存在删除list中,后台通过purge线程进行回收处理

以一条sql执行update、select过程,画图表示,如图:

执行update操作,事务A提交时候(事务还没提交),会将数据进行备份,备份到对应的undo buffer,Undo Log保存了未提交之前的操作日志,User表数据肯定就是持久保存到InnoDB的数据文件IBD,默认情况。

这时事务B进行查询操作,是直接读undo buffer缓存的,这时事务A还没提交事务,要回滚(rollback),是不读磁盘的,先直接从undo buffer缓存读取
在这里插入图片描述
undo log 有两种

  • insert undo log
    代表事务在 insert 新记录时产生的 undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  • update undo log
    事务在进行 update 或 delete 时产生的 undo log ; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除

在这里插入图片描述
我们看一下这个undo log的版本链,我来解释一下
undo log 采用链的形式来保存事务的变化,TRX_ID就是当前事务的编号,DB_ROLL_PTR就是上一个事务的版本变化的数据地址

最底下一行是一个原始数据,它是没有事务ID,也没有回滚指针,所以我们设置为NULL
在这里插入图片描述
(1)现在来了一个事务 A对该记录的 name 做出了修改,改为 “张三"

  • 在事务 A修改该行(记录)数据时,数据库会先对该行加排他锁
  • 然后把该行数据拷贝到 undo log 中,作为旧记录,既在 undo log 中有当前行的拷贝副本
  • 拷贝完毕后,修改该行name为张三,并且修改隐藏字段的事务 ID 为当前事务 A的 ID, 我们默认从 1 开始,之后递增,回滚指针指向拷贝到 undo log 的副本记录,既表示我的上一个版本就是它
  • 事务提交后,释放锁
    在这里插入图片描述

(2)又来了个事务 B修改stu 表的同一个记录,将name修改为 “张小三 ”

  • 在事务2修改该行数据时,数据库也先为该行加锁
  • 然后把该行数据拷贝到 undo log 中,作为旧记录,发现该行记录已经有 undo log 了,那么最新的旧数据作为链表的表头,插在该行记录的 undo log 最前面
  • 修改该行 name 为 张小三,并且修改隐藏字段的事务 ID 为当前事务 B的 ID, 那就是 2 ,回滚指针指向刚刚拷贝到 undo log 的副本记录
  • 事务提交,释放锁
    在这里插入图片描述
    然后类推就得到我们的undo log 链了

Read View

什么是 Read View?

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

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

Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的 DB_TRX_ID(即当前事务 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 所在的旧记录就是当前事务能看见的最新老版本

那判断条件是什么?

首先Read_View是一种数据结构,包含四个字段

m_ids:当前活跃的事务编号集合(活跃事务:表示的是还未提交的事务)
min_trx_id:最小活跃事务编号
max_trx_id:预分配事务编号,当前最大事务编号+1
creator_trx_id:Read_View创建者的事务编号(当我们执行一次快照读就会出现Read_View 比如select,本文章出现两次快照读,select1和select2)

我们知道了这些,我们就可以先分析

1.1 读已提交的情况下是怎么读的

RC:在每一次执行快照读时生成ReadView

还看之前的图(因为太大了,图片放上来不清晰,我截取一部分)
在这里插入图片描述

在这里插入图片描述

第一个黄色的框框对应select1,产生的快照读,别告诉我你不知道啥意思,我们之前说了,
select就是快照读
第二个黄色的框框对应select2,产生的快照读

结合我们之前的参数解释
在这里插入图片描述

在这里插入图片描述
第一次select读的Read_View
在这里插入图片描述

(1)

  • 当前事务ID为3,不等于创建事务ID,所以往下继续判断,但是如果等于是什么意思,就是自己这个事务先执行了一个update,然后立即执行了一个select,所以最新的数据版本号和我们select时候创建Read_View的ID是一样,当然可以读
  • 当我们继续往下判断的时候,判断当前事务ID为3,不小于最小的活跃事务ID,说明还未提交不可以访问,但是如果我们这成立那么代表数据已经提交,当然可以访问。
  • 当前线程ID为3当然是小于,最大事务编号+1也就是5,所以还要往下判断,但是 如果大于代表这当前事务是在Read_View生成以后才开启的,当然是不允许访问的
  • 最后判断,当前事务ID也就是3,如果它大于2小于5,那么就要判断它在不在m_ids里面(2,3,4),经过对比发现,它在里面,说明还没有被提交,所以不可以访问

(2)
从整个推导过程来看,当前事务3都不满足 ,所以沿着undo log往下找下一个
也就是当前事务变成2,然后进行判断

  • 2不等于4不符合
  • 2大于2 不符合
  • 2小于5 不符合
  • 2<=2<=5,然后判断是否在{2,3,5}里面,发现是在的,所以也是未提交的不满足

(3)
然后下一个就是事务ID为1的

  • 1不等于4 不符合
  • 1小于2符合 说明已经提交,可以访问到这个

所以我们的select * from stu where id =1088 的结果是:“张三”

第二次select读的Read_View
在这里插入图片描述

还是沿着我们之前的推导逻辑去走
我们的select * from stu where id =1088 的结果是:“张小三”

所以
RC级别:Select1=张三 Select2=张小三
也就是说读已提交不能解决,可重复读的问题

1.2 可重复读的情况下是怎么读的那

可重复读(RR):仅在第一次执行快照读的时候生成Read_View,后续快照读复用之前的快照读
在这里插入图片描述
也就是说这两次的Read_View用的是同一个,就不会产生不可重复度的情况了,规则是一样的,什么都没变的情况下,结果肯定是一样的

所以
RR级别:Select1=张三 Select2=张三

总结:
RC , RR 级别下的 InnoDB 快照读有什么不同?
正是 Read View 生成时机的不同,从而造成 RC , RR 级别下快照读的结果的不同

  • 在 RR 级别下的某个事务的对某条记录的第一次快照读会创建一个快照及 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个 Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个 Read View,所以对之后的修改不可见;
  • 即 RR 级别下,快照读生成 Read View 时,Read View 会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
  • 而在 RC 级别下的,事务中,每次快照读都会新生成一个快照和 Read View , 这就是我们在 RC 级别下的事务中可以看到别的事务提交的更新的原因

总之在 RC 隔离级别下,是每个快照读都会生成并获取最新的 Read View;而在 RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View, 之后的快照读获取的都是同一个 Read View。

可能存在的疑问:
RR级别下使用MVCC能避免幻读嘛?
答:能,但不完全能!
因为MVCC,连续多次快照读,Read_View会产生复用,没有幻读问题
比如:当两次快照读之间存在当前读,Read_View会重新生成,导致产生幻读
在这里插入图片描述
视频🔗:https://www.bilibili.com/video/BV1hL411479T?spm_id_from=333.337.search-card.all.click

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爪哇贡尘拾Miraitow

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值