【MySQL】MVCC多版本并发控制

目录

  • 一、数据库的并发场景
  • 二、多版本并发控制
    • 隐藏字段
    • undo日志
    • 模拟MVCC
    • Read View
    • 整体流程
    • RC(不可重复读)和RR(可重复读)的本质区别

一、数据库的并发场景

数据库并发的场景无非如下三种:

  • 读-读并发:不存在任何问题,也不需要并发控制。
  • 读-写并发:有线程安全问题,可能会存在事务隔离性问题,可能遇到脏读、幻读、不可重复读。
  • 写-写并发:有线程安全问题,可能会存在两类更新丢失问题。

说明一下:

  • 写-写并发场景下的第一类更新丢失又叫做回滚丢失,即一个事务的回滚把另一个已经提交的事务更新的数据覆盖了,第二类更新丢失又叫做覆盖丢失,即一个事务的提交把另一个已经提交的事务更新的数据覆盖了。
  • 读-读并发不需要进行并发控制,写-写并发实际也就是对数据进行加锁,这里最值得讨论的是读-写并发,读-写并发是数据库当中最高频的场景,在解决读-写并发时不仅需要考虑线程安全问题,还需要考虑并发的性能问题。

二、多版本并发控制

  • 多版本并发控制(Multi-Version Concurrency Control,MVCC)是一种用来解决读写冲突的无锁并发控制,主要依赖记录中的3个隐藏字段、undo日志和Read View实现。
  • 为事务分配单向增长的事务ID,为每个修改保存一个版本,将版本与事务ID相关联,读操作只读该事务开始前的数据库快照。
  • MVCC保证读写并发时,读操作不会阻塞写操作,写操作也不会阻塞读操作,提高了数据库并发读写的性能,同时还可以解决脏读、幻读和不可重复读等事务隔离性问题。

隐藏字段

数据库表中的每条记录都会有如下3个隐藏字段:

  • DB_TRX_ID:6字节,创建或最近一次修改该记录的事务ID。
  • DB_ROW_ID:6字节,隐含的自增ID(隐藏主键)。
  • DB_ROLL_PTR:7字节,回滚指针,指向这条记录的上一个版本。

说明一下:

  • 采用InnoDB存储引擎建立的每张表都会有一个主键,如果用户没有设置,InnoDB就会自动以DB_ROW_ID产生一个聚簇索引。
  • 此外,数据库表中的每条记录还有一个删除flag隐藏字段,用于表示该条记录是否被删除,便于进行数据回滚。

实例

创建一个学生表,表中包含学生的姓名和年龄。如下:
在这里插入图片描述
当向表中插入一条记录后,该记录不仅包含name和age字段,还包含三个隐藏字段。如下:

nameageDB_ROW_ID(创建该记录的事务ID)DB_ROW_ID(隐式主键)DB_ROLL_PTR(回滚指针)
张三2891null
  • 假设插入该记录的事务的事务ID为9,那么该记录的DB_TRX_ID字段填的就是9。
  • 因为这是插入的第一条记录,所以隐式主键DB_ROW_ID字段填的就是1。
  • 由于这条记录是新插入的,没有历史版本,所以回滚指针DB_ROLL_PTR的值设置为null。
  • MVCC重点需要的就是这三个隐藏字段,实际还有其他隐藏字段,只不过没有画出。

undo日志

  • redo log:重做日志,用于MySQL崩溃后进行数据恢复,保证数据的持久性。
  • bin log:逻辑日志,用于主从数据备份时进行数据同步,保证数据的一致性。
  • undo log:回滚日志,用于对已经执行的操作进行回滚,保证事务的原子性。

MySQL会为上述三大日志开辟对应的缓冲区,用于存储日志相关的信息,必要时会将缓冲区中的数据刷新到磁盘。

MVCC的实现主要依赖三大日志中的undo log,记录的历史版本就是存储在undo log对应的缓冲区中的。

在这里插入图片描述

模拟MVCC

现在有一个事务10(仅仅为了好区分),对student表中记录进行修改(update):将name(张三)改成name(李四)。

  • 事务10,因为要修改,所以要先给该记录加行锁。

  • 修改前,现将改行记录拷贝到undo log中,所以,undo log中就有了一行副本数据。(原理就是写时拷贝)

  • 所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name,改成 ‘李四’。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务10 的ID, 我们默认从 10 开始,之后递增。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。

  • 事务10提交,释放锁。

在这里插入图片描述
备注:此时,最新的记录是’李四‘那条记录。
现在又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)。

  • 事务11,因为也要修改,所以要先给该记录加行锁。(该记录是那条?)
  • 修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的副本,我们采用头插方式,插入undo log。
  • 现在修改原始记录中的age,改成 38。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务11 的ID。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副
  • 本记录,既表示我的上一个版本就是它。
  • 事务11提交,释放锁。

在这里插入图片描述
这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。

上面的一个一个版本,我们可以称之为一个一个的 快照

insert和delete的记录如何维护版本链?

  • 删除记录并不是真的把数据删除了,而是先将该记录拷贝一份放入undo log中,然后将该记录的删除flag隐藏字段设置为1,这样回滚后该记录的删除flag隐藏字段就又变回0了,相当于删除的数据又恢复了。
  • 新插入的记录是没有历史版本的,但是一般为了回滚操作,新插入的记录也需要拷贝一份放入undo log中,只不过被拷贝到undo log中的记录的删除flag隐藏字段被设置为1,这样回滚后就相当于新插入的数据就被删除了。

当前读、快照读

  • 当前读:读取最新的记录,就叫做当前读。
  • 快照读:读取历史版本,就叫做快照读。

事务在进行增删查改的时候,并不是都需要进行加锁保护:

  • 事务对数据进行增删改的时候,操作的都是最新记录,即当前读,需要进行加锁保护。
  • 事务在进行select查询的时候,既可能是当前读也可能是快照读,如果是当前读,那也需要进行加锁保护,但如果是快照读,那就不需要加锁,因为历史版本不会被修改,也就是可以并发执行,提高了效率,这也就是MVCC的意义所在。

而select查询时应该进行当前读还是快照读,则是由隔离级别决定的,在读未提交和串行化隔离级别下,进行的都是当前读,而在读提交和可重复读隔离级别下,既可能进行当前读也可能进行快照读。

Read View

事务在进行快照读操作时会生成读视图ReadView,在该事务执行快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃的事务ID。

ReadView在MySQL源码中就是一个类,本质是用来进行可见性判断的,当事务对某个记录执行快照读的时候,对该记录创建一个ReadView,根据这个Read View来判断,当前事务能够看到该记录的哪个版本的数据。

ReadView类的源码如下:

class ReadView {
	// 省略...
private:
	/** 高水位:大于等于这个ID的事务均不可见*/
	trx_id_t m_low_limit_id;
	
	/** 低水位:小于这个ID的事务均可见 */
	trx_id_t m_up_limit_id;
	
	/** 创建该 Read View 的事务ID*/
	trx_id_t m_creator_trx_id;
	
	/** 创建视图时的活跃事务id列表*/
	ids_t m_ids;
	
	/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
	* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
	trx_id_t m_low_limit_no;
	
	/** 标记视图是否被关闭*/
	bool m_closed;
	
	// 省略...
};

部分成员说明:

  • m_ids: 一张列表,记录Read View生成时刻,系统中活跃的事务ID。
  • m_up_limit_id: 记录m_ids列表中事务ID最小的ID。
  • m_low_limit_id: 记录Read View生成时刻,系统尚未分配的下一个事务ID。
  • m_creator_trx_id: 记录创建该Read View的事务的事务ID。

由于事务ID是单向增长的,因此根据Read View中的m_up_limit_id和m_low_limit_id,可以将事务ID分为三个部分:

  • 事务ID小于 m_up_limit 的事务,一定是生成Read View时已经提交的事务,因为m_up_limit_id是生成Read View时刻系统中活跃事务ID中的最小ID,因此事务ID比它小的事务在生成Read View时一定已经提交了。
  • 事务ID大于等于m_low_limit_id的事务,一定是生成Read View时还没有启动的事务,因为m_low_limit_id是生成Read View时刻,系统尚未分配的下一个事务ID。
  • 事务ID位于m_up_limit_id和m_low_limit_id之间的事务,在生成Read View时可能正处于活跃状态,也可能已经提交了,这时需要通过判断事务ID是否存在于m_ids中来判断该事务是否已经提交。

在这里插入图片描述

  • 一个事务在进行读操作时,只应该看到自己或已经提交的事务所作的修改,因此我们可以根据Read View来判断当前事务能否看到另一个事务所作的修改。
  • 版本链中的每个版本的记录都有自己的DB_TRX_ID,即创建或最近一次修改该记录的事务ID,因此可以依次遍历版本链中的各个版本,通过Read View来判断当前事务能否看到这个版本,如果不能则继续遍历下一个版本。

源码:

bool changes_visible(trx_id_t id, const table_name_t& name) const 
	MY_ATTRIBUTE((warn_unused_result))
{
	ut_ad(id > 0);
	//1、事务id小于m_up_limit_id(已提交)或事务id为创建该Read View的事务的id,则可见
	if (id < m_up_limit_id || id == m_creator_trx_id) {
		return(true);
	}
	check_trx_id_sanity(id, name);
	//2、事务id大于等于m_low_limit_id(生成Read View时还没有启动的事务),则不可见
	if (id >= m_low_limit_id) {
		return(false);
	}
	//3、事务id位于m_up_limit_id和m_low_limit_id之间,并且活跃事务id列表为空(即不在活跃列表中),则可见
	else if (m_ids.empty()) {
		return(true);
	}
	const ids_t::value_type* p = m_ids.data();
	//4、事务id位于m_up_limit_id和m_low_limit_id之间,如果在活跃事务id列表中则不可见,如果不在则可见
	return (!std::binary_search(p, p + m_ids.size(), id));
}

使用该函数时将版本的DB_TRX_ID传给参数id,该函数的作用就是根据Read View,判断当前事务能否看到这个版本。

如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件,即可以看到。上面的
readview 是当你进行select的时候,会自动形成。

整体流程

假设当前有条记录:
在这里插入图片描述
事务操作:
在这里插入图片描述

  • 事务4:修改name(张三) 变成name(李四)
  • 当事务2 对某行数据执行了 快照读 ,数据库为该行数据生成一个 Read View 读视图

//事务2的 Read View

m_ids; // 1,3

up_limit_id; // 1

low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID

creator_trx_id // 2

此时版本链是:
在这里插入图片描述

  • 只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务。
    在这里插入图片描述
    我们的事务2在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID 去跟up_limit_id,low_limit_id和活跃事务ID列表(trx_list) 进行比较,判断当前事务2能看到该记录的版本。

//事务2的 Read View

m_ids; // 1,3

up_limit_id; // 1

low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID

creator_trx_id // 2 //事务4提交的记录对应的事务ID

DB_TRX_ID=4

//比较步骤

DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步

DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步

m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中。

//结论

故,事务4的更改,应该看到。 所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本

实例:
在这里插入图片描述

RC(不可重复读)和RR(可重复读)的本质区别

实例

不可重复读
在这里插入图片描述
在两个事务并发运行时,一个事务可以观察到另一个事务操作数据的过程。

可重复读

情况1:
在这里插入图片描述

情况2:
在这里插入图片描述

上面两次实验的唯一区别在于,右终端中的事务在左终端中的事务修改数据之前是否进行过快照读。

RR与RC的本质区别

  • 正是因为Read View生成时机的不同,从而造成了RC和RR级别下快照读的结果的不同。
  • 在RR级别下,事务第一次进行快照读时会创建一个Read View,将当前系统中活跃的事务记录下来,此后再进行快照读时就会直接使用这个Read View进行可见性判断,因此当前事务看不到第一次快照读之后其他事务所作的修改。
  • 而在RC级别下,事务每次进行快照读时都会创建一个Read View,然后根据这个Read View进行可见性判断,因此每次快照读时都能读取到被提交了的最新的数据。
  • RR级别下快照读只会创建一次Read View,所以RR级别是可重复读的,而RC级别下每次快照读都会创建新的Read View,所以RC级别是不可重复读的。

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

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

相关文章

Ubuntu搭建国标平台wvp-GB28181-pro

目录 简介安装和编译1.查看操作系统信息2.安装最新版的nodejs3.安装java环境4.安装mysql5.安装redis6.安装编译器7.安装cmake8.安装依赖库9.编译ZLMediaKit9.1.编译结果说明 10.编译wvp-GB28181-pro10.1.编译结果说明 配置1.WVP-PRO配置文件1.1.Mysql数据库配置1.2.REDIS数据库…

Ubuntu 隐藏Telnet主机SSH服务时显示版本信息问题

一、背景 默认情况下&#xff0c;我们通过telnet服务器的22端口&#xff0c;能够获取OpenSSH服务的banner信息(如下图所示)。而低版本的OpenSSH存在许多高危漏洞。。为了安全我们要隐藏这个信息。 二、隐藏Telnet版本信息 当使用telnet命令&#xff0c;telnet 192.168.31.20…

python打造光斑处理系统2:打开图像和默认图像

文章目录 打开图像默认图像 光斑处理&#xff1a;python处理高斯光束的图像 光斑处理系统&#xff1a;程序框架 打开图像 光斑图像的本质是光强在空间中的分布&#xff0c;而有的时候&#xff0c;通过CCD拍到的图像往往存成虚假的RGB格式&#xff0c;所以在打开图像时&#x…

超越人类上限的策划:百度输入法在候选词区域植入广告

一位 V2EX 用户最新发帖称&#xff0c;百度输入法的最新版本中引入了一个新功能&#xff0c;将广告直接植入到候选词区域。 具体表现为&#xff0c;当用户输入某些关键词时&#xff0c;候选词区域会显示与输入内容相关的广告链接。例如&#xff0c;用户输入“招商”时&#xf…

Elasticsearch:如何为 Elastic Stack 配置 AI Assistant

了解并安装 Elastic AI Assistant Elastic 推出了 Observability AI Assistant&#xff0c;这是一款利用生成式 AI 来增强你的 Observability 体验的强大工具。 该 AI 助手由 OpenAI 或 Azure OpenAI 服务的连接器提供支持&#xff0c;可带来上下文洞察和聊天功能&#xff0c;…

企业如何遵守数据安全法规进行SAP数据脱敏处理?

为什么谈SAP数据脱敏&#xff1f; SAP进入中国市场超过30年&#xff0c;作为一个典型的德国思维的产品&#xff0c;我们很难找到另一个市值这么大的软件&#xff0c;在过去的几十年中&#xff0c;一直活的这么滋润&#xff0c;没有太多的挑战&#xff0c;而且软件本身的变化肉眼…

Java-Swing

Swing 课程&#xff1a;14、Swing之JFrame窗体_哔哩哔哩_bilibili 一.JFrame和容器 构造方法 Constructor and DescriptionJFrame() 构造一个最初不可见的新框架。 JFrame(GraphicsConfiguration gc) 在屏幕设备的指定 GraphicsConfiguration中创建一个 Frame和一个空白标题。…

Linux系统——正则表达式

有一段时间本机访问量过高&#xff0c;如何查看日志提取出访问量前十的信息 1.使用提取命令&#xff08;cut、awk、sed&#xff09;提取出ip地址的那一列 2.使用sort按数字排序&#xff0c;将相同的地址整合到一起 3.使用uniq -c统计出数量 4.使用sort 数字 数字倒序排序 5.最…

使用OpenCV实现一个简单的实时人脸跟踪

简介&#xff1a; 这个项目将通过使用OpenCV库来进行实时人脸跟踪。实时人脸跟踪是一项在实际应用中非常有用的技术&#xff0c;如视频通话、智能监控等。我们将使用OpenCV中的VideoCapture()函数来读取视频流&#xff0c;并使用之前加载的Haar特征级联分类器来进行人脸跟踪。 …

[React源码解析] Fiber

在React15及以前, Reconciler采用递归的方式创建虚拟Dom, 但是递归过程不可以中断, 如果组件的层级比较深的话, 递归会占用线程很多时间, 那么会造成卡顿。 为了解决这个问题, React16将递归的无法中断的更新重构为异步的可中断更新, Fiber架构诞生。 文章目录 1.Fiber的结构2…

22.云原生之GitLab CICD实战及解析【干货】

云原生专栏大纲 文章目录 准备工作gitlab-ci.yml流水线mven打包项目制作并推送镜像kaniko方式docker方式 部署到k8s验证执行情况 GitLab Runner k8s执行器工作流程注册配置kubernetes runnerkubernetes runner配置通过修改 Pod 规范为每个构建作业创建一个 PVC自定义卷装载持久…

C++ Qt开发:运用QJSON模块解析数据

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍如何运用QJson组件的实现对JSON文本的灵活解析…

【python】求矩阵的特征值和特征向量

使用np.linalg.eig同时求特征值和特征向量 import numpy as np#输入矩阵 A np.array([[1, 1/2, 1/6, 1/9],[2, 1, 1/3, 1/5],[6, 3,1,1/2],[9, 5,2,1]])#求解特征值和其对应的特征向量 eigval,eigvec np.linalg.eig(A) for i in range(len(eigval)):print(f特征值&#xff1a…

Flask使用Jinja2渲染模版使用变量实战

前言&#xff1a; Flask 使用 Jinja2 作为其默认模板引擎&#xff0c;这意味着您可以直接在 Flask 应用程序中使用 Jinja2 模板。您可以创建模板文件&#xff0c;然后在视图函数中渲染这些模板&#xff0c;将动态数据传递给模板进行渲染&#xff0c;并最终生成最终的 HTML 页面…

349. 两个数组的交集(力扣LeetCode)

文章目录 349. 两个数组的交集题目描述数组解题set容器解题该思路数组版解题 349. 两个数组的交集 题目描述 给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 示例 1&#xff1a; 输入&a…

MATLAB知识点:创建MATLAB的脚本

​讲解视频&#xff1a;可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇&#xff08;数学建模清风主讲&#xff0c;适合零基础同学观看&#xff09;_哔哩哔哩_bilibili 节选自第2章 在实际应用中&#xff0c;直接在命令行窗口中输…

Phoncent博客,探索Rie Kudan的GPT创作之举

近日&#xff0c;大家都在谈论日本作家Rie Kudan&#xff0c;她凭借其小说《东京共鸣塔》&#xff08;"Tokyo-to Dojo-to"&#xff09;荣获了日本极具声望的芥川奖。这本小说引起了广泛的讨论和思考&#xff0c;因为令人惊讶的是&#xff0c;Kudan在其中直接引用了人…

伊恩·斯图尔特《改变世界的17个方程》毕达哥拉斯定理笔记

它告诉我们什么&#xff1f; 直角三角形的三个边之间有什么关系。 为什么重要&#xff1f; 它提供了几何和代数之间的重要联系&#xff0c;使我们能够根据坐标计算距离。它也催生出了三角学。 它带来了什么&#xff1f; 测绘、导航&#xff0c;以及较近代出现的狭义和广义相对论…

solr的原理是什么

1 Java程序里如果有无限for循环的代码导致CPU负载超高&#xff0c;如何排查&#xff1f; 排查Java程序中由于无限循环导致的CPU负载过高的问题&#xff0c;可以按照以下步骤进行&#xff1a; 资源监控&#xff1a; 使用系统命令行工具&#xff08;如Linux上的top或htop&#xf…

Pytest 识别case规则

一、Python测试框架&#xff0c;主要特点有以下几点&#xff1a; 简单灵活&#xff0c;容易上手&#xff1b;支持参数化&#xff1b;能够支持简单的单元测试和复杂的功能测试&#xff0c;还可以用来做selenium/appnium等自动化测试、接口自动化测试&#xff08;pytestrequests…