详解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.mzph.cn/news/742870.shtml

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

相关文章

ChromeDriver 122 版本为例 国内下载地址及安装教程

ChromeDriver 国内下载地址 https://chromedriver.com/download 靠谱 千千万万别下载错了 先确认 Chrome 浏览器版本 以 win64 版本为例 那我们下载这一个啊&#xff0c;不要下载错了 下载地址贴在这哈 https://storage.googleapis.com/chrome-for-testing-public/122.0.…

GPT-3.5发布:大型语言模型的进化与挑战

摘要&#xff1a; GPT-3.5是OpenAI于2023年发布的一款大型语言模型&#xff0c;它是GPT-3的升级版&#xff0c;拥有1750亿个参数&#xff0c;比GPT-3的参数量增加了近一倍。GPT-3.5在文本生成、对话系统、文本理解等任务上表现出色&#xff0c;其性能已经接近甚至超过了人类水…

机器学习实验------Python机器学习软件包Scikit-Learn的学习与运用

第1关&#xff1a;使用scikit-learn导入数据集 本关任务 本关任务是使用scikit-learn的datasets模块导入iris数据集&#xff0c;并打印前5条原数据、前5条数据标签及原数据的数组大小。 即编程实现step1/importData.py 的getIrisData()函数&#xff1a; from sklearn import…

低代码开发平台,快速搭建开源MES系统

MS低代码云MES作为一家专注于提供生产制造数字化方案的服务商&#xff0c;“以客户为中心”、以“数据驱动、智能化、互联化”为企业的核心标签&#xff0c;以低代码平台为切入点&#xff0c;帮助企业构建以人为本的未来供应链生态系统&#xff0c;实现制造企业的智能化转型。 …

深度复制:C# 中 List 与 List 多层嵌套不改变原值的实现方法

概述&#xff1a;以上内容详细介绍了在 C# 中实现不改变原 List 值的多层嵌套复制方法&#xff0c;包括使用 AutoMapper、Json.NET、以及对象序列化的步骤和示例。这些方法提供了灵活而高效的方式&#xff0c;可以根据项目需求选择最适合的深度复制方式。 1. 使用 AutoMapper …

Vulnhub - Toppo

希望和各位大佬一起学习&#xff0c;如果文章内容有错请多多指正&#xff0c;谢谢&#xff01; 个人博客链接&#xff1a;CH4SER的个人BLOG – Welcome To Ch4sers Blog Toppo 靶机下载地址&#xff1a;Toppo: 1 ~ VulnHub 0x01 信息收集 Nmap扫描目标主机&#xff0c;发…

Component和Loader在QML中是紧密相关的两个元素,它们常常一起使用来实现动态加载和实例化QML组件的功能

Component 是一个可重用的QML组件定义&#xff0c;它描述了一个独立的UI元素及其行为。可以将Component看作是一个模板或蓝图&#xff0c;用于创建多个相同类型的QML对象实例。 Loader 是一个特殊的QML元素&#xff0c;用于动态加载和实例化QML组件。它允许您根据需要在运行时…

关于分布式微服务数据源加密配置以及取巧方案(含自定义加密配置)

文章目录 前言Spring Cloud 第一代1、创建config server项目并加入加解密key2、启动项目&#xff0c;进行数据加密3、实际项目中的测试server Spring Cloud Alibaba低版本架构不支持&#xff0c;取巧实现无加密配置&#xff0c;联调环境问题加密数据源配置原理探究自定义加密解…

ubuntu 20.04 Python pip 配置 pip.conf

1. 状况描述 $ pip install timm WARNING: Retrying (Retry(total4, connectNone, readNone, redirectNone, statusNone)) after connection broken by ProxyError(Cannot connect to proxy., RemoteDisconnected(Remote end closed connection without response)): /simple/t…

ubuntu22.04环境中安装pylint

ubuntu22.04环境中安装pylint sudo apt-get install python3-pipsudo aptitude install python3-pipsudo pip install pylint sudo apt-get install python3-pip 在安装pylint的时候&#xff0c;需要使用pip命令&#xff0c;在ubuntu22.04环境中命令如下&#xff1a; $ sudo …

[LeetCode][110]平衡二叉树

题目 110.平衡二叉树 给定一个二叉树&#xff0c;判断它是否是平衡二叉树。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;true 示例 2&#xff1a; 输入&#xff1a;root [1,2,2,3,3,null,null,4,4] 输出&#xff1a;false 示例 3&…

Linux:1_常见指令以及权限理解(上)

常见指令以及权限理解 一.补充知识 为方便初学者更好的理解Linux系统,这部分将对比windows系统补充一部分必要知识1 2 3 4 5 6 二.补充指令 1. 重新认识指令: 指令本质都是程序 —指令、程序、可执行程序都是一回事(都是文件内容属性)指令就是程序 … 安装和卸载是在把可…

安卓Java面试题 101- 110

101. Android中touch事件的传递机制是怎样的?1.Touch事件传递的相关API有dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent 2.Touch事件相关的类有View、ViewGroup、Activity 3.Touch事件会被封装成MotionEvent对象,该对象封装了手势按下、移动、松开等动作 4.Touch…

分布式搜索引擎elasticsearch(2)

1.DSL查询文档 elasticsearch的查询依然是基于JSON风格的DSL来实现的。 1.1.DSL查询分类 Elasticsearch提供了基于JSON的DSL&#xff08;[Domain Specific Language](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html)&#xff09;来定义查…

(一)RabbitMQ实战——rabbitmq的核心组件及其工作原理介绍

前言 RabbitMQ是一个开源的消息代理软件&#xff0c;它实现了高级消息队列协议&#xff08;AMQP&#xff09;标准&#xff0c;提供可靠的消息传递机制。RabbitMQ可以用于在应用程序之间传递消息&#xff0c;实现不同应用系统之间的解耦和通信。它支持多种编程语言&#xff0c;…

什么是Ribbon,怎么实现负载均衡?

一. Ribbon 是 Netflix 公司开发的一个负载均衡器&#xff08;Load Balancer&#xff09;工具&#xff0c;主要用于在分布式系统中进行客户端侧的负载均衡。它可以集成到微服务架构中的客户端&#xff0c;通过在客户端实现负载均衡算法&#xff0c;来分发请求到多个服务提供者…

彩虹知识付费模板MangoA全开源包含秒杀/抽奖/社群/推送等功能

二次开发增加以下功能每日秒杀每日签到官方社群多级分销在线抽奖项目投稿 每日秒杀 每日签到 官方社群 多级分销 在线抽奖 项目投稿 下载地址&#xff1a;https://pan.xunlei.com/s/VNstMfOecGliiqew7UIorsOnA1?pwdhywi#

<支持向量机算法(SVM:Support Vector Machine)>——《机器学习算法初识》

目录 一、⽀持向量机(SVM)算法 1 SVM算法导⼊ 2 SVM算法定义 2.1 定义 2.2 超平⾯最⼤间隔介绍 2.3 硬间隔和软间隔 2.3.1 硬间隔分类 2.3.2 软间隔分类 3 ⼩结 二、 SVM算法api初步使⽤ 三、 SVM算法原理 1 定义输⼊数据 2 线性可分⽀持向量机 3 SVM的计算过程与算…

Rust 构建开源 Pingora 框架可以与nginx媲美

一、概述 Cloudflare 为何弃用 Nginx&#xff0c;选择使用 Rust 重新构建新的代理 Pingora 框架。Cloudflare 成立于2010年&#xff0c;是一家领先的云服务提供商&#xff0c;专注于内容分发网络&#xff08;CDN&#xff09;和分布式域名解析。它提供一系列安全和性能优化服务…

亚马逊云科技 Lambda 运行selenium

有些定时任务需要使用自动化测试的工具&#xff0c;如果使用亚马逊云科技 Lambda来实现这个功能的话&#xff0c;那么就需要图形框架&#xff0c;而我们知道lambda其实是一个虚拟机&#xff0c;而且按照系统级别依赖比较困难。所以这里选择使用容器的形式进行发布。 在dockerf…