MySQL多版本并发控制MVCC及实现原理

前提概要

什么是MVCC?
MVCC(Multi-Version Concurrency Control,多版本并发控制)是数据库管理系统用于数据并发访问的技术。它允许读写并发进行,而不用传统的阻塞,加锁保护。

MVCC的核心思想就是记录多个版本,使读写能够看到不同的版本,从而避免读写之间相互影响。当然MVCC主要针对的就是读写。

MVCC能够解决什么问题?
在数据库并发的场景主要有以下三种:

  • 读 - 写并发:存在问题,会存在事务隔离性问题。出现幻读、不可重复读、脏读等。
  • 读 - 读并发:不存在什么问题。
  • 写 - 写并发:存在问题,第一类更新丢失,第二类更新丢失。

其中写写存在的问题主要是:

1)会回滚已经事务,导致已经提交的事务数据丢失。

2)会覆盖数据。导致已经提交的事务数据被另一个事务覆盖。

所以针对写 - 写一般是通过排他锁(X锁)来维护。

而读 - 写是mysql使用频率最高的用法,单纯的加锁是无法满足高频使用。一种无所并发的机制多版本控制MVCC就被人们运用到读 - 写当中。

MVCC有什么好处?
MVCC主要针对的是读 - 写,进一步说是在MySQL InnoDB下主要是针对RR 和 RC这俩种机制。

MVCC的好处:

  • 高并发的无所版本,能够大幅度提升读写的效率。
  • 解决了脏读、幻读、不可重复读等问题。但是无法解决更新丢失问题。

小结:

MVCC是多版本控制版本,通过线性增长的时间戳记录修改过的历史版本,让读和写看到不同的版本,从而用来解决读 - 写效率低的问题。


MVCC的实现原理

MVCC的实现原理主要是依靠3个隐藏字段、Undo日志、ReadView实现。

三个隐藏字段

在数据记录中,每一份记录不仅包含表中的字段。还有三个隐藏的字段构成。 

  • DB_TRX_ID:6 byte       记录最近修改(insert/update)事务的ID(会自动为每一个事物生成一个线性增长的ID)。
  • DB_ROLL_PTR : 7 byte  回滚指针 记录数据的上一个版本(也就是undo log中)。
  • DB_ROW_ID : 6 byte      自增主键,如果表中没有创建主键,会默认生成一个隐藏主键。

实际上,还有一个隐藏字段,就是flag,标记字段是否被删除。

举例:创建一个表,并且插入一条消息。显示当前的结果。

补全隐藏字段:

idnameDB_TRX_IDDB_ROLL_PTRDB_ROW_ID
1张三nullnull1

 这一条数据的事物ID我们是不知道的就设为null(当然你也可以默认设为1),由于也不知道隐藏自增主键默认设为1,这条数据是最新的记录没有历史版本,所以回滚指针也会null。


undo日志

undo日志:又叫做回滚日志,是MySQL在内存中开辟的一块缓冲区。用于保存历史版本的数据,必要的时候,会将数据刷新到磁盘中。

一、假如有一个事务往student表中插入一条新记录,id为1 ,name 为张三,事务的id为null,隐式主键生成为1,没有历史的版本,回滚指针为null。

二、现在事务1对表中的数据修改,将name 修改为李四

  • 事务1修改一条记录时,会先增加排他锁。
  • 将旧的记录拷贝到nudo日志上。
  • 拷贝完成后,修改隐藏字段,将事务ID修改为当前ID为。回滚指针指向undo日志上刚拷贝的记录地址。
  • 最后释放锁。

三、事务2,对表中的数据修改,将id修改为2。

事务对表中的数据修改时,会先加排他锁。

将数据拷贝到undo日志中。

拷贝完毕后,对当前的数据修改。将事务id修改为2 ,回滚指针指向刚拷贝的数据地址。最后将id修改为2。

释放锁。

当前事务或者不同事务对数据修改之后,就会在undo日志中形成一条历史版本,并且最新的数据会链接到历史的版本,这就是版本链。

undo日志的数据什么时候删除? 

一般是在事务提交,并且其它事务没有关联到这条数据时,就能删除。

补充说明

  1. 所谓的数据回滚,实际上就是将版本链中的数据删除并且替换成版本链的起始点。
  2. 而保存回滚节点,实际上就是标记指定的版本链。
  3. 这种技术是基于写时拷贝,当涉及CURD时候,就会拷贝一份旧的数据记录到undo日志中。

insert和delete是如何保存在undo日志呢?

insert的对立面操作是delete,delete的对立面操作是insert。

这俩种操作会用到flag删除标记位。

  • 删除并没有实际的把数据删掉,而是通过写时候拷贝在undo日志中保存一份记录,然后将数据中的flag设位为1,表示删除字段。如果数据回滚,那么它的标记会就会回到0。
  • 插入应该是没有历史版本的,但是为了回滚数据,也会写时拷贝一份插入数据到undo日志中,并且将flag标记位设位1。一旦发生回滚,就会把拷贝flag为1的数据,属于删除标记,不会显示。


快照读vs当前读

上述的实验证明:数据的增加、删除、更新都是会产生版本链的。都是要加锁保护。

但是select读取呢?需要加锁吗??不需要!

因为在undo日志中维护了历史和最近的版本。

什么是当前读?读取到的最新的数据,增加、删除、更新后立马就能读取到

快照读:读取的不是最新的。换句话而言,读取到版本链中的数据就是快照读。

我们不难发现,像增加、删除、更新等都是涉及当前读,是需要加锁保护的。如果读取的是当前最新的消息,就需要串行化。效率是很低的。

但是对于快照读,因为维护了历史版本,是不需要加锁保护的。

换言之 ,快照读是可以进行并发读和写


Read View读视图

什么决定了select是当前读还是快照读?事务的隔离级别

事务总是分为执行前 执行中(begin -> CURD -> commit) 执行后

先来的事务,先开启的时候,和后来的事务,它们看到的数据有可能是不同的。

比如事务先进行CURD并且提交后,这时候来的事务是不是应该看到。

那应该如何保证事务应该看到什么,不应该看到什么?就是由read view决定的。

什么是Read View?

read view是事务在进行快照读的时候,会生成的一个读视图。

本质上read view是一个类对象,类中会进行当前活跃的事务id与自身事务id。

read view也是事务可见性判断标准。

read view管理的成员

m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID(没有写错)
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的
最大值+1(也没有写错)
creator_trx_id //创建该ReadView的事务ID

详细介绍

m_ids:活跃的事务集合

up_limit_id:活跃事务集中最小的事务id

low_limit_id:read view系统尚未分配的事务id编号

creator_trx_id:当前事务的活跃ID


read view判断可见性的规则

来自@呵呵一笑百媚生一张源码图 。

事务的id可以划分为3个部分

比最小事务集合中id还要小的事务、正在执行的事务、 尚未分配的事务id

  •  如果当前id比最小的up_limit_id还要小,说明在进行快照的时候,这份事务已经执行完毕,那肯定是可以看到的。
  • 如果当前id比尚未分配的事务 low_limit_id还要大,说明在快照读的时候,当前事务还没开始(即在快照读之后,立马进行的插入更新删除),是不能被看到的。
  • 如果事务id在[up_limit_id,low_limit_id)之前,就要继续比较。判断是否存在于正在活跃的事务id中,如果存在,就说明我已经进行快照读了,你是和我同时执行的事务,还没有commit不应该被看到。如果不存在于活跃id中,那就就说明快照读的时候,已经提交了,可以被看到。

注意:

  1. 处于正在活跃的事务id,不一定是连续的,应该如果该事务已经提交,就不是活跃事务。
  2. 如果正在活跃的事务id是3,4,7,8,9。尚未分配的事务id是11
  3. 则说明:事务 1 2 是已经commit 的事务id ,事务5 6 10也是已经commit都应该被看到。反之,其它的事务都不应该被看到。

MVCC整体流程

模拟一下快照读下的MVCC实现过程,有利于更深入的理解。

1)假设当前有一条记录

nameageDB_TRX_ID(创建该记录的事
务ID)
DB_ROW_ID(隐式
主键)
DB_ROLL_PTR(回滚
指针)
张三28null1null

2)现有有四个事务分别对这张表数据进行CURD,这些事务在同一时刻启动。

事务1 [id=1]事务2 [id=2]事务3 [id=3]事务4 [id=4]
事务开始事务开始事务开始事务开始
.........修改且已提交
进行中快照读进行中
.........

事务4将张三修改为李四,并且提交了事务。(这个时候,修改的操作会生成版本链,尽管提交事务,但是有其它事务关联,也不会删除版本链)。

事务2进行快照读,数据库会生成read view 读视图

那么read view的成员各为什么呢?

  • 活跃的事务集 1 2 3 (因为在快照读之前,事务4已经提交,不属于活跃的事务)
  • up_limit_id :活跃事务集中最小的事务id 应该是 1
  • low_limit_id:事务中系统尚未分配的最小事务id  5   (4已经被创建过,尽管不在活跃集中)。
  • 当前的事务id :2

这时候的版本链:

应该记录历史name为数据记录。

视图read view对象就会拿着版本链的数据和自己记录的id比较。

比较规则:

  •  DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步
  • DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步
  • m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中。

当前数据事务id为4,比较up_limit 不小于。所以事务4的启动没有早于快照读的事务,进行下一步判断。

比较low_limit 不为尚未分配的事务。说明在快照读的时候,事务4已经存在。

比较是否在活跃集中,活跃集为 1 2 3,不在。说明快照读的时候,事务4 已经提交了记录。

所以这一条记录是应该被快照读所看到的。

而事务4 提交的数据,站在全局的角度上也是最新的版本,事务2 能读到最新的版本。


RR 与 RC的本质区别

做一个实对比RR和RC的差别,用的知识(当前读的操作select lock in share mode)

当前读和快照读在RR下的差别(1)
事务A事务B
beginbegin
select * from 快照读 name=张三select* from 快照读 name=张三
更新name=李四
提交事务
select* from 快照读 name=李四
select lock in share mode(当前读)name=李四

这里符合我们的预期,因为是RR级别,所以我们认为只有在事务B也提交之后,才能读到最新的消息。

快照读和当前读在RR下的区别(2)
事务A事务B
beginbegin
select * from 快照读 name=张三
更新name=李四
提交事务
select* from 快照读 name=张三
select lock in share mode(当前读)name=李四

为什么事务B在没有提交就能读取到最新的消息呢?相信你如果认真看了MVCC的整体流程一定不会有疑惑的,因为read view的判断决定了能看到事务A提交的结果。

快照读非常依赖读取的时机!即首次出现的数据,决定了后续快照读的内容。

本质区别

在Read View下生成的时机不同,会导致快照读的内容不同。

RR下,首次进行快照读的时候,会生成一份read view对象,MySQL会保存起来,用于后续快照读的比较判断。

所以在后续的时间段内,都是同一份read view。

而RC下,每一次快照读都会生成一份read view对象,并且覆盖成为最新的。MySQL的比较就会拿着最新的read view与版本链比较。

正因为RC下每一次快照读都会生成一个read view 就会导致出现不可重复读的问题。

所以本质的区别就是快照读是否为最新的!

  • 29
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

深度搜索

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

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

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

打赏作者

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

抵扣说明:

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

余额充值