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

2024-03-14 09:36

本文主要是介绍详解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

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

这篇关于详解MySQL的MVCC(ReadView部分解析C++源码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/807979

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

SQL中的外键约束

外键约束用于表示两张表中的指标连接关系。外键约束的作用主要有以下三点: 1.确保子表中的某个字段(外键)只能引用父表中的有效记录2.主表中的列被删除时,子表中的关联列也会被删除3.主表中的列更新时,子表中的关联元素也会被更新 子表中的元素指向主表 以下是一个外键约束的实例展示

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

如何去写一手好SQL

MySQL性能 最大数据量 抛开数据量和并发数,谈性能都是耍流氓。MySQL没有限制单表最大记录数,它取决于操作系统对文件大小的限制。 《阿里巴巴Java开发手册》提出单表行数超过500万行或者单表容量超过2GB,才推荐分库分表。性能由综合因素决定,抛开业务复杂度,影响程度依次是硬件配置、MySQL配置、数据表设计、索引优化。500万这个值仅供参考,并非铁律。 博主曾经操作过超过4亿行数据

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

MySQL数据库宕机,启动不起来,教你一招搞定!

作者介绍:老苏,10余年DBA工作运维经验,擅长Oracle、MySQL、PG、Mongodb数据库运维(如安装迁移,性能优化、故障应急处理等)公众号:老苏畅谈运维欢迎关注本人公众号,更多精彩与您分享。 MySQL数据库宕机,数据页损坏问题,启动不起来,该如何排查和解决,本文将为你说明具体的排查过程。 查看MySQL error日志 查看 MySQL error日志,排查哪个表(表空间

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof