详解MySQL的MVCC(ReadView部分解析C++源码)

文章目录

    • 1. 什么是MVCC
    • 2. MVCC核心组成(三大件)
      • 2.1 MVCC为什么需要三大件
    • 3. 隐藏字段
    • 4. undo log
      • 4.1 模拟版本链数据形成过程
    • 5. Read View
      • 5.1 m_ids
      • 5.2 m_creator_trx_id
      • 5.3 m_low_limit_id
      • 5.4 m_up_limit_id
      • 5.5 可见性分析算法
    • 6. MVCC流程模拟
      • 6.1 RC隔离级别
      • 6.2 RR隔离级别

  1. 以下讨论均建立在innodb存储引擎
  2. ReadView部分官方文档基本都是一笔带过,为了保证文章的正确性,这部分涉及到大量的源码分析

1. 什么是MVCC

一种数据库中用于处理并发读写事务的技术。它通过维护数据的不同版本来实现对同一数据项的并发访问,并且在保证事务隔离性的同时,允许读操作无需加锁就能获取一致性的数据视图。

2. MVCC核心组成(三大件)

在这里插入图片描述
MVCC的实现离不开上图所示的三大组件

  • 数据行的隐藏字段
  • undo log
  • read view

2.1 MVCC为什么需要三大件

我们按照逻辑来简单梳理下,为什么MVCC需要这三个部分。

首先,在并发情况下,MVCC需要维护数据行的多个版本,那多个版本信息存储在哪呢?我们可以率先排除数据库表,数据库表是存储索引+已提交数据的,再多维护一份历史数据,.idb文件要骂人了(.idb是innodb下存储表数据的文件后缀)。为了减轻.idb文件的压力,我们不妨在多创建一个文件,用于存储数据行的多个版本,而这便是undo log

我们知道多版本数据存储在undo log中,和数据表中的数据是分开存储的,那么每行数据如何找到它的历史版本呢?MySQL将数据写入数据库文件时,为多创建几个字段列,其中一个字段,就是专门存储指针,用于指向历史版本的指针。这也就是隐藏字段存在的原因

现在,MySQL的行数据能够关联到历史数据,那每个事务在查询时,获取哪个版本的数据呢?read view就是提供了数据版本确定的依据。再查询数据时,MySQL会生成read view,read view提供了选择哪个版本数据的依据

3. 隐藏字段

在这里插入图片描述

  • DB_TRX_ID:

    • 这是一个6字节长的字段,用来记录最后一次插入或更新该行记录的事务标识符。当某一行被删除时,内部会将其视为一种特殊的更新操作,在行的一个特定位上设置标记以表示其已被删除。
  • DB_ROLL_PTR:

    • 也称为回滚指针,是一个7字节长的字段。它指向回滚段中的一个undo日志记录。如果该行被更新,则undo日志记录包含重建更新前行内容所需的信息。当需要进行事务回滚或者为了实现一致性读时,可以通过回滚指针找到并应用相应的undo日志。
  • DB_ROW_ID:

    • 这是一个6字节长的行ID字段,每当新行插入时,它的值就会单调递增。如果InnoDB自动生成聚簇索引,那么这个聚集索引中就会包含行ID值。否则,DB_ROW_ID列不会出现在任何索引中。行ID可以帮助系统在没有用户定义唯一键的情况下为行提供一个唯一的标识,并且在某些查询和排序场景下也能发挥作用。

4. undo log

undo log是回滚日志,其中记录包含了关于如何撤销事务对聚集索引记录所做的最新更改的信息。事务在对数据操作前,undolog会记录数据原本的数值,方便事务回滚,回复到上一个版本的数据

undo log日志会准备两份,一份用于记录insert操作;另一份记录update 或者 delete操作

在不指定的情况下,undo log默认会在data文件夹下创建,并命名为undo_001,undo_002

在这里插入图片描述

tip:

  1. 当insert时,undo log日志只会在回滚时需要,事务提交后可被立即删除
  2. 当update、delete时,undo log日志不仅在回滚时需要、快照读时也需要,不会被立刻删除

4.1 模拟版本链数据形成过程

我们配合隐藏字段,通过undo log来模拟版本链形成的过程

  • step 1

在这里插入图片描述

一开始,事务1插入一条id = 4,age = 18的数据,数据隐藏字段DB_TRX_ID = 1(当前事务id = 1),DB_ROLL_PTR = null(没有历史版本)。此时事务2、3、4、5全部开启事务(begin)

  • step 2

在这里插入图片描述

事务2执行update操作,将id = 1数据行的age字段设置为20。MySQL更新逻辑是,先更新buffer pool中缓存的数据页,形成脏页。当事务提交后,将数据刷到磁盘中。

此时,MySQL会将当前记录存储到undo log中,用于数据回滚,其地址为0x001

  • step 3

在这里插入图片描述

事务2执行commit操作,buffer pool中的脏页被持久化到磁盘中。数据被永久更改。一方面age被设置为20,另一方面DB_TRX_ID(最新修改的事务id)设置为2,DB_ROLL_PTR指向上一个数据版本,也就是0x001位置上的数据,形成版本链

  • step 4

在这里插入图片描述
事务3执行update操作,修改缓存中的数据,同时将当前记录写入到undo log中,其地址为0x002

  • step 5

在这里插入图片描述

事务3提交事务,数据被持久化到磁盘。当前记录更改,age设置为70,DB_TRX_ID设置为3,DB_ROLL_PTR指向上一个版本的数据——0x002,追加版本链信息

  • step 6

在这里插入图片描述
事务4执行update逻辑,修改内存中的数据,并将当前记录写入undo log,为其分配地址0x003

  • step 7

在这里插入图片描述
事务4提交事务,数据被持久化。age被修改为50,DB_TRX_ID设置为当前事务id,也就是4,DB_ROLL_PTR指向上个版本的数据——0x003。至此,完整的版本连形成。

ps: 终于写完这部分了,光这个图就累死我了

5. Read View

readView视图存在如下4个字段

  • m_ids:存储当前快照生成时,所有活跃事务id
  • m_low_limit_id(max_trx_id):预分配事务id,等于当前最大事务id + 1(事务是自增的)
  • m_up_limit_id(min_trx_id):最小活跃事务id
  • m_creator_trx_id:ReadView创建者的id

在这里插入图片描述
源码分析

5.1 m_ids

	/** Set of RW transactions that was active when this snapshotwas taken */ids_t		m_ids;

m_ids类型是ids_tids_t是ReadView的一个内部类

在这里插入图片描述
ids_t类中维护如下数据

		/** Memory for the array */value_type*	m_ptr;/** Number of active elements in the array */ulint		m_size;/** Size of m_ptr in elements */ulint		m_reserved;friend class ReadView;
  • m_ptr指向的是一块连续的内存空间,也就是数组。数组中每个元素的类型是value_type,追溯源码后发现value_type是无符号64位整数。为了方便后文叙述,我们把m_ptr当成数组

    tip: ids_t维护m_ptr的增删改等方法,使得数组能够自动扩容

  • m_size维护数组的大小,ulint也是无符号64位整数

  • m_reserved维护m_ptr中元素个数

现在,我们可以初步总结m_ids的作用。m_ids中维护了一个数组,而该数组保存了ReadView生成时,活跃的事务id。我们可以把m_ids看作成Java里的List集合

此外,还得强调一点,m_ids中存储id是按照递增顺序

在阅读源码时,笔者注意到ids_t类中声明的insert方法

在这里插入图片描述

注释中的preserving the order表明,ids_t这个类维护的数据是有序的,但因为ids_t这个类是申明在read0types.h这个头文件中,insert方法没给出实现,因此我们需要找到insert方法实现的类,阅读insert的具体代码,这样我们才能得知ids_t维护的数据是以什么顺序存储的

定位到read0read.cc,我们找到insert的实现

在这里插入图片描述
笔者已经对部分代码做出中文注解,故额外的逻辑不再赘述,感兴趣的读者可以自行阅读。

我们主要关注定位的逻辑

	value_type*	ub = std::upper_bound(data(), end, value);if (ub == end) {push_back(value); } 

源码中调用std标准库提供的upper_bound函数,该函数底层是二分查找,目的是在给定地址范围内,找到大于目标数据的地址,并返回。如果找不到,则返回末尾迭代器(指向数组的最后一个元素的下一个位置)

insert方法中,upper_bound的目的是在m_ptr指向的数组中,找到一个最小大于value的元素,并返回它的地址。在源码中,value_type* ub就是指向value应该插入的地址。

如果ub指向end,也就是数组末尾,这意味着m_ptr指向的数组中,不存在大于value的元素,需要将value插入的数组末尾。这说明m_ptr数组中,最大的元素存储在末尾,这就表明了ids_t维护的数组是升序

5.2 m_creator_trx_id

这个字段没啥好说的,谁创建ReadView,就赋值谁的id

5.3 m_low_limit_id

	/** The read should not see any transaction with trx id >= thisvalue. In other words, this is the "high water mark". */trx_id_t	m_low_limit_id;

我们注意到,注释中并未直接解释m_low_limit_id是什么含义,我们只知道如当事务id如果 >= readView.m_low_limit_id,事务是不可见的。

m_low_limit_id就是高水位,事务id 大于这个值,那么事务就是不可见的。但它为什么是预分配事务id呢?

我们定位到m_low_limit_id赋值的地方

在这里插入图片描述
m_low_limit_id = trx_sys->max_trx_id这行代码可知,m_low_limit_id的值是事务系统中的max_trx_id

我们继续查看max_trx_id相关代码

发现如下信息

在这里插入图片描述
max_trx_id 是还未被分配的最小事务id,也就是预分配id,同样也可以被等价理解为最大事务id(因为事务id是自增的,还未被分配的自然是已有的事务id的最大值)

因此我们得出结论,m_low_limit_id基本等价于max_trx_id,是预分配的id,等于已存在最大事务id + 1

5.4 m_up_limit_id

由源码注释可知,m_up_limit_id是"低水位",事务id小于这个值的,都是可见的

在这里插入图片描述

但它为什么是当前活跃事务的最小事务id呢?

我们定位到赋值时的源码

在这里插入图片描述
由注释可知,m_up_limit_id存储的是最小活跃id。其实看到注释就已经很明白了,但笔者想要结合前文说到m_ids的一个特点来讲解我们如何得到这个结论。

观察这段代码 m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id;,我们发现在正常情况下,m_up_limit_id取的是m_ids.front()

m_ids.front()弹出的是数组第一个元素,m_ids维护的数据是升序!!!,因此第一个元素就是当前活跃的最小id
在这里插入图片描述

tip: m_ids数组中的id是升序,这个结论在m_ids这个章节已经详细介绍,这里不在赘述

5.5 可见性分析算法

在这里插入图片描述
可见性分析算法,能够判断undo log版本链上的版本是否可见。MySQL需要将版本对应的事务id和ReadView中的数据进行判断。

判断规则总结如下

  1. id == m_creator_trx_id ? 可以访问该版本。因为数据就是当前事务更改的
  2. id < m_up_limit_id (min_trx_id)?可以访问该版本,此时数据已经commit了
  3. id > m_low_limit_id(max_trx_id)?不能访问该版本,当前事务是在ReadView创建后开启的
  4. id不在m_ids中?可以访问该版本,因为m_ids存储活跃id,不活跃说明已经commit了

6. MVCC流程模拟

至此,我们介绍完MVCC所有组件,现在我们复用"模拟版本连数据形成"章节出现的案例,结合ReadView讲解MVCC是如何工作的

tip:
当事务的隔离级别是READ UNCOMMITED,SERIALIZABLE时,MVCC基本不起作用

  • READ UNCOMMITED,读未提交。都读未提交了,并发管理个锤子。全部脏读,爱咋地咋地
  • SERIALIZABLE,串行化。直接加锁锁定数据,只允许有一个事务控制被修改的数据,其他事务都得等着。这有个锤子的并发

6.1 RC隔离级别

RC隔离级别,每次select都会创建新的ReadView

我们以事务5第一次select时的情况分析

在这里插入图片描述
此时我们将ReadView和undo log版本链上的数据进行比对,调用可见性分析算法分析当前undo log数据链上的版本,哪个是可见的

在这里插入图片描述
首先是版本链上最新的数据,也就是地址在0x002的数据。这个数据版本是被trx_id = 2的事务创建。trx_id = 2和ReadView进行分析,我们发现 id < min_trx_id,因此当前数据可以被访问。

检查原图也好理解,在事务5 select之前,数据就已经被事务2提交了。因此,本次查询到的数据是
id = 1, age = 20

在这里插入图片描述


现在,我们以事务5第二次select为例,进行分析

在这里插入图片描述
我们将undo log版本链上的数据和ReadView中的数据进行比较

在这里插入图片描述
我们发现0x003上的数据,trx_id = 3,小于ReadView的min_trx_id。这也就说明,0x003版本的数据在ReadView生成之前被创建,可以被访问。

在这里插入图片描述
检查原图,逻辑无误。因此,本次查询到的数据是
id = 1, age = 70

6.2 RR隔离级别

在RR隔离级别下,ReadView只会以第一次创建的ReadView为准。因此事务5第一次查询,ReadView视图和RC级别下的第一次查询没有区别。查询得到的数据是id = 1,age = 20

但RR级别下,第二次的ReadView依然不变。此时ReadView和undo log情况如下图所示

在这里插入图片描述
首先判断0x003的数据,trx_id = 3不满足ReadView中的任何可见性判断规则,因此判断0x002版本的数据。

0x002版本的数据trx_id = 2,小于ReadView的min_trx_id,因此可见,所以此时查询到的数据依然是id = 1, age = 20

这也就实现了可重复读隔离级别

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/536258.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Axure高保真原型】下拉列表切换图表

今天和大家分享通过下拉列表动态切换统计图表的原型模板&#xff0c;我们可以通过下拉列表选择要显示的图表&#xff0c;包括柱状图、条形图、饼图、环形图、折线图、曲线图、面积图、阶梯图、雷达图&#xff1b;而且图表数据可以在左侧表格中动态维护&#xff0c;包括增加修改…

浅谈Redis 的 保护模式(protected-mode)

今天在一台服务器上面部署了redis,发现始终无法用工具远程连接,项目里面是正常的,就是工具不行,防火墙也关闭了.折腾了一会才突然想起来,是不是触发了保护模式. 什么时候触发保护模式protected-mode: 同时满足以下两个: 1.bind未指定ip 2.未配置密码 解决方案: 编辑redis…

码头船只出行和货柜管理系统的设计与实现

针对于码头船只货柜信息管理方面的不规范&#xff0c;容错率低&#xff0c;管理人员处理数据费工费时&#xff0c;采用新开发的码头船只货柜管理系统可以从根源上规范整个数据处理流程。 码头船只货柜管理系统能够实现货柜管理&#xff0c;路线管理&#xff0c;新闻管理&#…

数据结构之树(Topk问题, 链式二叉树)

一.topk问题 取N个数中最大(小)的前k个值,N远大于k 这道题可以用堆的方法来解决,首先取这N个数的前k个值,用它们建堆 时间复杂度O(k) 之后将剩余的N-k个数据依次与堆顶数据进行比较,如果比堆顶数据大,则将堆顶数据覆盖后向下调整 时间复杂度(N-k)*log(N) 总共的时间复杂度…

[SAP ABAP] 数值向上/向下取整

ceil()函数对数值进行向上取整&#xff0c;floor()函数对数值进行向下取整 输出结果&#xff1a;

48. 【Linux教程】yum 软件包管理

本小节介绍如何在 Linux 系统中使用 yum 命令软件管理。 1.yum 简介 yum 是 Red Hat 软件包管理器&#xff0c;它能够查询有关可用软件包的信息&#xff0c;从存储库获取软件包&#xff0c;安装和卸载软件包&#xff0c;以及将整个系统更新到最新的可用版本。yum 在更新&#…

力扣-[700. 二叉搜索树中的搜索]

递归法 确定递归函数的参数和返回值 递归函数的参数传入的就是根节点和要搜索的数值&#xff0c;返回的就是以这个搜索数值所在的节点。 代码如下&#xff1a; public TreeNode searchBST(TreeNode root, int val) 确定终止条件 如果root为空&#xff0c;返回null&#xff0c…

ChatGPT4 API-Key初探-本地调用API进行多轮对话方和流式输出

ChatGPT API 使用&#x1f60e; 既然要做大模型尽管可能国内无法使用GPT进行商用&#xff0c;除了性能地表最强之外&#xff0c;其优秀的Function calling&#xff08;函数调用&#xff09;在线微调还有各种&#xff0c;例如可以便捷处理知识向量库等功能&#xff0c;都是非常…

OpenHarmony开源项目—工程管理

DevEco Studio的基本使用&#xff0c;请参考DevEco Studio使用指南。本章主要介绍如何使用DevEco Studio进行多设备应用开发。 说明&#xff1a; 本章的内容基于DevEco Studio 3.1.1 Release版本进行介绍&#xff0c;如您使用DevEco Studio其它版本&#xff0c;可能存在文档与产…

Spring boot 集成netty实现websocket通信

一、netty介绍 Netty 是一个基于NIO的客户、服务器端的编程框架&#xff0c;使用Netty 可以确保你快速和简单的开发出一个网络应用&#xff0c;例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程&#xff0c;例如&#xff1a;基于TCP和U…

新手教程科普,手把手教你在bitget walelt用Uniswap

什么是 Uniswap (UNI)&#xff1f; Uniswap 是以太坊区块链上领先的去中心化交易所 (DEX)&#xff0c;允许用户交换和交易各种加密货币。 Uniswap 于 2018 年 11 月推出&#xff0c;是以太坊最早的 DApp 之一。目前有四个版本&#xff1b;其中V1、V2、V3都有开源代码库。 与 C…

JS数组相关知识

获取数组的最大值/最小值&#xff1a; let arrary [2,5,4] let max arrary[0] for(let i 0;i<arrary.length;i){if(arrary[i]>max){max arrary[i]} }console.log(max);//查询数组最小值let arr [2,21,34,23,45] let min arr[0] for(let i 0;i<arr.length;i){if…