详解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 snapshot
	was 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 >= this
	value. 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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Java】容器|Set、List、Map及常用API

目录 一、概述 二、List 1、List的常用API 2、ArrayList 3、List遍历 三、Set 1、Set的常用方法: 2、HashSet 3、遍历集合&#xff1a; 四、Map 1、Map常用API 2、HashMap 3、遍历Map 五、迭代器 一、概述 在Java中所有的容器都属于Collection接口下的内容 1…

【PyTorch】成功解决ModuleNotFoundError: No module named ‘torch’

【PyTorch】成功解决ModuleNotFoundError: No module named ‘torch’ &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希…

Oracle19c静默部署

Oracle19c静默部署文档 下载地址 https://www.oracle.com/database/technologies/oracle-database-software-downloads.html#db_free 一、系统基础配置 1、创建用户和用户组 # 创建oinstall和dba用户组 groupadd oinstall groupadd dba# 创建Oracle用户 useradd -g oinstall…

一起学数据分析_3(模型建立与评估_1)

使用前面清洗好的数据来建立模型。使用自变量数据来预测是否存活&#xff08;因变量&#xff09;&#xff1f; &#xff08;根据问题特征&#xff0c;选择合适的算法&#xff09;算法选择路径&#xff1a; 1.切割训练集与测试集 import pandas as pd import numpy as np impo…

Linux第78步_使用原子整型操作来实现“互斥访问”共享资源

使用原子操作来实现“互斥访问”LED灯设备&#xff0c;目的是每次只允许一个应用程序使用LED灯。 1、创建MyAtomicLED目录 输入“cd /home/zgq/linux/Linux_Drivers/回车” 切换到“/home/zgq/linux/Linux_Drivers/”目录 输入“mkdir MyAtomicLED回车”&#xff0c;创建MyA…

【数据结构和算法初阶(C语言)】二叉树铺垫--栈帧的创建与销毁--细节全解

前言&#xff1a; 学习这么久以来&#xff0c;可能有很多疑问&#xff1a;局部变量怎么创建的&#xff1f;为什么局部变量的值是随机的&#xff1f;函数是怎么传参的&#xff1f;传参的顺序是怎么样的&#xff1f;形参和实参是什么样的关系&#xff1f;函数调用是怎么做的&…

App的测试,和传统软件测试有哪些区别?增加哪些方面的测试用例

从上图可知&#xff0c;测试人员所测项目占比中&#xff0c;App测试占比是最高的。 这就意味着学习期间&#xff0c;我们要花最多的精力去学App的各类测试。也意味着我们找工作前&#xff0c;就得知道&#xff0c;App的测试点是什么&#xff0c;App功能我们得会测试&#xff0…

CTF-SHOW-摆烂杯-电子取证

&#x1f36c; 博主介绍 博主介绍&#xff1a;大家好&#xff0c;我是 Mikey &#xff0c;很高兴认识大家~ 主攻&#xff1a;【应急响应】 【python】 【数字取证】【单机取证】【流量分析】【MISC】 &#x1f389;点赞➕评论➕收藏 养成习惯&#xff08;一键三连&#xff0…

JAVA实战开源项目:高校学院网站(Vue+SpringBoot)

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 学院院系模块2.2 竞赛报名模块2.3 教育教学模块2.4 招生就业模块2.5 实时信息模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 学院院系表3.2.2 竞赛报名表3.2.3 教育教学表3.2.4 招生就业表3.2.5 实时信息表 四、系…

数据结构课程设计实践--哈夫曼树

1.问题描述 2.需求分析 2.1 功能需求 设计一个哈夫曼的编/译码系统&#xff0c;实现下述功能&#xff1a; 初始化&#xff0c;从终端读入字符集大小n&#xff0c;以及n个字符和n个权值&#xff0c;建立哈夫曼树&#xff0c;并将它存于文件hfmTree中&#xff1b;编码&#xf…

RIPGeo参文33、37(最大化原始图与摄动图之间的一致性):保证不相关信息的消除

RIPGeo中有: 训练目标为: 由于需要准确的地理定位预测,损失鼓励图表示学习来保持IP地理定位的基本信号。同时,和通过最大化原始图与摄动图[33]、[37]之间的一致性,保证了不相关信息的消除。考虑到模型在训练早期的不稳定性,我们不直接同时优化多个训练目标。相反,我们采…

物联网竞赛板CubMx全部功能简洁配置汇总

目录 前言&#xff1a;1、按键&LED灯配置&#xff1a;2、OLED配置&#xff1a;3、继电器配置&#xff1a;4、LORA模块配置&#xff1a;5、矩阵模块&#xff1a;6、串口模块&#xff1a;7、RTC配置&#xff1a;8、ADC模块配置&#xff1a;9、温度传感器模块&#xff1a;后续…

3.Windows下安装MongoDB和Compass教程

Windows下安装MongoDB 总体体验下来&#xff0c;&#xff0c;要比MySQL的安装简单了许多&#xff0c;没有过多的配置&#xff0c;直接就上手了&#xff01; 1、下载 进入官方的下载页面https://www.mongodb.com/try/download/community&#xff0c;如下选择&#xff0c;我选…

【热门话题】前端框架发展史

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 前端开发的历史演变引言第一章&#xff1a;起源与基础建设 - HTML与CSS时代1.1 …

python 爬取人民新闻

基础信息获取&#xff1a; 要闻url&#xff1a;https://www.gov.cn/yaowen/liebiao/home.htm 下一页的url&#xff1a;https://www.gov.cn/yaowen/liebiao/home_1.htm 基础代码&#xff1a; import re import openpyxl import requests from lxml import etree import osdef …

MySQL 篇- Java 连接 MySQL 数据库并实现数据交互

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 JDBC 概述 2.0 实现 Java 连接 MySQL 数据库并实现数据交互的完整过程 2.1 安装数据库驱动包 2.2 创建数据源对象 2.3 获取数据库连接对象 2.4 创建 SQL 语句 2.…

基于Java+SpringMvc+vue+element实现驾校管理系统详细设计

基于JavaSpringMvcvueelement实现驾校管理系统详细设计 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐留言 文末…

SQLiteC/C++接口详细介绍之sqlite3类(十二)

返回目录&#xff1a;SQLite—免费开源数据库系列文章目录 上一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;十一&#xff09; 下一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;十三&#xff09; ​37.sqlite3_load_extension 用于在SQLit…

鸿蒙Harmony应用开发—ArkTS声明式开发(基础手势:Web)下篇

onRequestSelected onRequestSelected(callback: () > void) 当Web组件获得焦点时触发该回调。 示例&#xff1a; // xxx.ets import web_webview from ohos.web.webviewEntry Component struct WebComponent {controller: web_webview.WebviewController new web_webv…

如果要做优化,CSS提高性能的方法有哪些?

文章目录 一、前言二、实现方式内联首屏关键CSS异步加载CSS资源压缩合理使用选择器减少使用昂贵的属性不要使用import其他 三、总结参考文献 一、前言 每一个网页都离不开css&#xff0c;但是很多人又认为&#xff0c;css主要是用来完成页面布局的&#xff0c;像一些细节或者优…