【MySQL】- 04 MVCC实现原理

news2024/12/23 14:16:54

MVCC的实现原理

  • 隐式字段
  • undo日志
  • Read View(读视图)
  • 整体流程
  • 例子

MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。所以我们先来看看这个三个point的概念

隐式字段

  • MySQL中会为每一行记录生成隐藏列,接下来就让我们了解一下这几个隐藏列吧。

    (1)DB_TRX_ID:事务ID,是根据事务产生时间顺序自动递增的,是独一无二的。如果某个事务执行过程中对该记录执行了增、删、改操作,那么InnoDB存储引擎就会记录下该条事务的id。

    (2)DB_ROLL_PTR:回滚指针,本质上就是一个指向记录对应的undo log的一个指针,大小为 7 个字节,InnoDB 便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在undo log中都通过链表的形式组织。

    (3)DB_ROW_ID:行标识(隐藏单调自增 ID),如果表没有主键,InnoDB 会自动生成一个隐藏主键,大小为 6 字节。如果数据表没有设置主键,会以它产生聚簇索引。

    (4)实际还有一个删除flag隐藏字段,既记录被更新或删除并不代表真的删除,而是删除flag变了。

请添加图片描述

如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本

undo日志

  • 每当我们要对一条记录做改动时(这里的改动可以指INSERT、DELETE、UPDATE),都需要把回滚时所需的东西记录下来, 比如:

    • Insert undo log :插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。
    • Delete undo log:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
    • Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。
      InnoDB把这些为了回滚而记录的这些东西称之为undo log。这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo log

    每次对记录进行改动都会记录一条undo日志,每条undo日志也都有一个DB_ROLL_PTR属性,可以将这些undo日志都连起来,串成一个链表,形成版本链。版本链的头节点就是当前记录最新的值。不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

purge

  • 从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
  • 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

对MVCC有帮助的实质是update undo log ,undo log实际上就是存在rollback segment中旧记录链,它的执行流程如下:

一、 比如一个有个事务插入persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL

在这里插入图片描述

二、 现在来了一个事务1对该记录的name做出了修改,改为Tom

  • 在事务1修改该行(记录)数据时,数据库会先对该行加排他锁

  • 然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本

  • 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它

  • 事务提交后,释放锁
    在这里插入图片描述

三、 又来了个事务2修改person表的同一个记录,将age修改为30岁

  • 在事务2修改该行数据时,数据库也先为该行加锁

  • 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面

  • 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录

  • 事务提交,释放锁

在这里插入图片描述

从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录(当然就像之前说的该undo log的节点可能是会purge线程清除掉,向图中的第一条insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里)

Read View(读视图)

在可重复读隔离级别下,我们可以把每一次普通的select查询(不加for update语句)当作一次快照读,而快照便是进行select的那一刻,生成的当前数据库系统中所有未提交的事务id数组(数组里最小的idmin_id)和已经创建的最大事务idmax_id)的集合,即我们所说的一致性视图readview。在进行快照读的过程中要根据一定的规则将版本链中每个版本的事务idreadview进行匹配查询我们需要的结果。

快照读是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。MVCC只在 READ COMMITTEDREPEATABLE READ两个隔离级别下工作,其他两个隔离级别不和MVCC不兼容。因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行,而SERIALIZABLE 则会对所有读取的行都加锁。事务的快照时间点(即下文中说到的Read View的生成时间)是以第一个select来确认的。所以即便事务先开始,但是select在后面的事务的update之类的语句后进行,那么它是可以获取前面的事务的对应的数据。

RC和RR隔离级别下的快照读和当前读:RC隔离级别下,快照读和当前读结果一样,都是读取已提交的最新;RR隔离级别下,当前读结果是其他事务已经提交的最新结果,快照读是读当前事务之前读到的结果。RR下创建快照读的时机决定了读到的版本。

对于使用RC和RR隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的。核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。为此,InnoDB提出了一个Read View的概念。

Read View就是事务进行快照读(普通select查询)操作的时候生产的一致性读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,它由执行查询时所有未提交的事务id数组(数组里最小的id为min_id)和已经创建的最大事务id(max_id)组成,查询的数据结果需要跟read view做对比从而得到快照结果。

在这里插入图片描述

版本链比对规则:

  1. 如果落在绿色部分(trx_id<min_id),表示这个版本是已经提交的事务生成的,这个数据是可见的;
  2. 如果落在红色部分(trx_id>max_id),表示这个版本是由将来启动的事务生成的,是肯定不可见的;
  3. 如果落在黄色部分(min_id<=trx_id<=max_id),那就包含两种情况:
    a.若row的trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;如果是自己的事务,则是可见的;
    b.若row的trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。

那么这个判断条件是什么呢?

/** Check whether the changes by id are visible.
	@param[in]	id	transaction id to check against the view
	@param[in]	name	table name
	@return whether the view sees the modifications of id. */
	bool changes_visible(
		trx_id_t		id, //当前事务id
		const table_name_t&	name) const
		MY_ATTRIBUTE((warn_unused_result))
	{
		ut_ad(id > 0);

		if (id < m_up_limit_id || id == m_creator_trx_id) {

			return(true);
		}

		check_trx_id_sanity(id, name);

		if (id >= m_low_limit_id) {

			return(false);

		} else if (m_ids.empty()) {

			return(true);
		}

		const ids_t::value_type*	p = m_ids.data();

		return(!std::binary_search(p, p + m_ids.size(), id));
	}

如上,它是一段MySQL判断可见性的一段源码,即changes_visible方法,该方法展示了我们拿DB_TRX_ID去跟Read View某些属性进行怎么样的比较

在展示之前,我先简化一下Read View,我们可以把Read View简单的理解成有三个全局属性

m_ids(trx_list)
一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID
up_limit_id
记录trx_list列表中事务ID最小的ID
low_limit_id
ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1

  • 首先比较DB_TRX_ID < up_limit_id, 如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断
  • 接下来判断 DB_TRX_ID 大于等于 low_limit_id , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
  • 判断m_ids.empty()如果空则说明,你这个事务在Read View生成之前就已经Commit了,你修改的结果,我当前事务是能看见的

整体流程

我们在了解了隐式字段,undo log, 以及Read View的概念之后,就可以来看看MVCC实现的整体流程是怎么样了

整体的流程是怎么样的呢?我们可以模拟一下

  • 当事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图,假设当前事务ID为2,此时还有事务1和事务3在活跃中,事务4在事务2快照读前一刻提交更新了,所以Read View记录了系统当前活跃事务1,3的ID,维护在一个列表上,假设我们称为trx_list
    在这里插入图片描述

  • Read View不仅仅会通过一个列表trx_list来维护事务2执行快照读那刻系统正活跃的事务ID,还会有两个属性up_limit_id(记录trx_list列表中事务ID最小的ID),low_limit_id(记录trx_list列表中事务ID最大的ID,也有人说快照读那刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的最大值+1,我更倾向于后者;所以在这里例子中up_limit_id就是1,low_limit_id就是4 + 1 = 5,trx_list集合的值是1,3,Read View如下图
    在这里插入图片描述

  • 我们的例子中,只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,所以当前该行当前数据的undo log如下图所示;我们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id,low_limit_id和活跃事务ID列表(trx_list)进行比较,判断当前事务2能看到该记录的版本是哪个。

在这里插入图片描述

  • 所以先拿该记录DB_TRX_ID字段记录的事务ID 4去跟Read View的的up_limit_id比较,看4是否小于up_limit_id(1),所以不符合条件,继续判断 4 是否大于等于 low_limit_id(5),也不符合条件,最后判断4是否处于trx_list中的活跃事务, 最后发现事务ID为4的事务不在当前活跃事务列表中, 符合可见性条件,所以事务4修改后提交的最新结果对事务2快照读时是可见的,所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lgJv0w9D-1685702218501)(mdpic/3133209-be5885051c52fb6a.png)]

  • 也正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同

例子

首先我们要准备两张表,一张test和一张account表,然后我们以accountundo log来画版本链,准备数据和原始记录图如下

//test表中数据
id=1,c1='11';
id=5,c1='22';
//account表数据
id=1,name=‘lilei’;

在这里插入图片描述

如下图,我们将按照里面的顺序执行sql
在这里插入图片描述

当我们执行到第7行的select的语句时,会生成readview[100,200],300,版本链如图所示:
在这里插入图片描述

此时我们查询到的数据为lilei300。我们首先要拿最新版本的数据trx_id=300readview中匹配,落在黄色区间内,一看该数据已经提交了,所以是可见的。继续往下执行,当执行到第10行的select语句时,因为trx_id=100并未提交,所以版本链依然为readview[100,200],300,版本链如图所示:
在这里插入图片描述

此时我们查询到的数据为lilei300。我们按上边操作,从最新版本依次往下匹配,我们首先要拿最新版本的数据trx_id=100readview中匹配,落在黄色区间内,一看该数据在未提交的数组中,且不是自己的事务,所以是不可见的;然后我们选择前一个版本的数据,结果同上;继续向上找,当找到trx_id=300的数据时,会落在黄色区间,且是提交的,所以数据可见。继续往下执行,当执行到第13行的select语句时,此时尽管trx_id=100已经提交了,因为是InnoDB的RR模式,所以readview不会更改,仍为readview[100,200],300,版本链如图所示:
在这里插入图片描述

此时我们查询到的数据为lilei300。原因同上边的步骤,不再赘述。

当执行update语句时,都是先读后写的,而这个读,是当前读,只能读当前的值,跟readview查找时的快照读区分开。

刚才演示的是InnoDB下的RR模式,接下来我们简单说一下RC模式,上文中提到的RC模式的数据读都是读最新的即当前读,所以readview是实时生成的,执行语句如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2m5mUftI-1685702218502)(mdpic/383a87fe-d5a8-47b7-bc0e-b4b5dd53ba22.jpg)]

当我们执行到第13行的select的语句时,会生成readview[200],300,版本链还和之前一样,此时我们查询到的数据为lilei2。原因和上边讲的RR模式下的比对规则相同。

此处我们演示的是update的情况,对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位上写上true,来表示当前记录已经被删除,在查询时按照上边的规则查到对应的记录,如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。

大家应该还关心一个问题,即undo log什么时候删除呢?系统会判断,没有比这个undo log更早的read view的时候,undo log会被删除。所以这里也就是为什么我们建议你尽量不要使用长事务的原因。长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

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

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

相关文章

Linux笔记-so.1和so的区别(三方程序链接Qt的so.1和so)

如下面这个程序使用ldd查看下&#xff1a; 从中可以看到一些so.6和so.1这些&#xff0c;这些其实是版本号&#xff0c;比如这个&#xff1a;/lib64/libstdc.so.6 可见so.版本号其实是个软连接&#xff0c;最终会连接到/lib64/libstdc.so.6.0.19 而CQt框架写的lib如下pro文件会生…

磁盘配额与进阶文件系统管理(一)

磁盘配额Quota 用途 针对www server&#xff0c;例如 每个人网页空间的容量限制&#xff1b;针对mail server&#xff0c;例如 每个人的邮件空间限制&#xff1b;针对file server&#xff0c;例如 每个人最大可用的网络硬盘空间&#xff1b;限制某一群组所能使用的最大磁盘空…

使用zerotier进行内网穿透,外网访问其它电脑上的虚拟机

目标 使用一台电脑&#xff0c;使用vmware创建三台虚拟机&#xff0c;处于同一网段。另一台电脑外网进行访问其虚拟机 用途 学习K8s集群&#xff0c;由于个人财力有限&#xff0c;云服务器买不了几台&#xff0c;而且不同厂家的云服务器无法做到内网互通 完成后缺陷 使用z…

nginx系统优化和内核优化

nginx系统优化 一&#xff1a;隐藏nginx版本号 方法一&#xff1a;修改配置文件 vim /usr/local/nginx/conf/nginx.confnginx -t systemctl restart nginx curl -I http://192.168.52.108方法二&#xff1a;修改源代码 vim /opt/nginx-1.24.0/src/core/nginx.h ##配置文件里…

逆向汇编与反汇编——汇编基础快速入门

一、常用32位寄存器介绍 不同位数的寄存器的名称&#xff1a; eax&#xff1a;累加寄存器。通常用于算数运算&#xff0c;将结果保留在eax当中&#xff0c;当然也可以用于其他用途&#xff0c;比如一般把返回值通过eax传递出去。 ebx&#xff1a;基址寄存器 。有点类似于ebp…

Go语言日志库logrus

Go语言日志库logrus 1、介绍 logrus logrus是目前Github上star数量最多的日志包&#xff0c;功能强大、性能高效、高度灵活&#xff0c;还提供了自定义插件的功能。很 多优秀的开源项目&#xff0c;例如&#xff1a;docker、prometheus等都使用了logrus。logrus除了具有日志…

VMware nat模式配置

使用nat模式&#xff0c;需要配置ip才能做到虚拟机与主机、外网正常通信 步骤 1 选择虚拟机设置&#xff0c;将网络连接改为nat模式 2 查看主机vmware network adpter vmnet8 打开控制面板。选择网络连接&#xff0c;右击vmnet8&#xff0c;打开属性 选择ip4&#xff0c;双击…

kubesphere插件,应用商店,应用仓库

应用商店 参考 步骤 以platform-admin角色的账号(admin)登录kubesphere点击右上角 “平台管理”点击“集群管理”点击 “自定义资源 CRD”搜索 clusterconfiguration点击 ClusterConfiguration点击 ks-installer 右侧的三个点&#xff0c;点击“编辑文件”在YAML 文件中&…

Linux进程间通信【匿名管道】

✨个人主页&#xff1a; 北 海 &#x1f389;所属专栏&#xff1a; Linux学习之旅 &#x1f383;操作环境&#xff1a; CentOS 7.6 阿里云远程服务器 文章目录 &#x1f307;前言&#x1f3d9;️正文1、进程间通信相关概念1.1、目的1.2、发展1.3、分类 2、什么是管道&#xff1…

Redi事务,数据持久化

4.其他数据功能 4.1pubsub发布订阅 Redis 发布订阅(pub/sub)是一种消息通信模式&#xff1a;发送者(pub)发送消息&#xff0c;订阅者(sub)接收消息。 Redis 客户端可以订阅任意数量的频道。 下面示例展示了频道 channel1 &#xff0c; 以及订阅这个频道的三个客户端 —— c…

旧改周报--深圳7大项目获进展:中海、星河等主导

2023年第22周&#xff0c;深圳市共发布拆除重建类权利人核实公示1项&#xff0c;规划公告1项&#xff0c;规划修改&#xff08;草案&#xff09;公示2项&#xff0c;规划&#xff08;修改&#xff09;公告1项&#xff0c;实施主体公示1项&#xff0c;建设用地批复1项&#xff0…

win7安装visual studio 2015出现安装包丢失或损坏

winr 输入 certmgr.msc 查看有没有选中的两个证书&#xff0c;如果没有需要从其他电脑导入&#xff0c;然后直接点击安装界面重试&#xff0c;即可继续安装

【SpringMVC】RESTful案例

1、Rest风格 对于Rest风格&#xff0c;我们需要学习的内容包括: REST简介REST入门案例REST快速开发案例:基于RESTful页面数据交互 1. REST简介 REST&#xff08;Representational State Transfer&#xff09;&#xff0c;表现形式状态转换,它是一种软件架构风格 当我们想表示…

优秀的测试开发应该具备的六大能力有哪些?

目录 前言 什么是测试开发工程师&#xff1f; 测试开发的六大能力 前言 从我工作中接触到的测试开发&#xff0c;以及面试测试开发候选人时问的问题&#xff0c;我将自己对测试开发这个岗位的理解&#xff0c;总结了如下六点能力。 我个人认为&#xff0c;具备如下六点能力…

数据结构之二叉树,实现二叉树的创建与遍历,以及二叉树的一些练习题

目录 目录 一、二叉树的创建与遍历 1.创建二叉树 构建出来的树如图&#xff1a; 2.二叉树的销毁 3.二叉树的前序遍历[Leetcode144.力扣] 4.二叉树的中序遍历 5.二叉树的后序遍历 二、二叉树的实现 1.获取树中节点的个数 2.获取叶子节点的个数 3.获取第K层节点的个数…

Redis实现分布式锁的原理:常见问题解析及解决方案、源码解析Redisson的使用

0、引言&#xff1a;分布式锁的引出 锁常常用于多线程并发的场景下保证数据的一致性&#xff0c;例如防止超卖、一人一单等场景需求 。通过加锁可以解决在单机情况下安全问题&#xff0c;但是在集群模式下就不行了。集群模式&#xff0c;即部署了多个服务器、并配置了负载均衡后…

记录使用Echarts-gl实现3D地图

一、前言 最近项目需要做个大屏展示的&#xff0c;开始做了第一版用户觉得地图太过于单调了&#xff0c;给我发了一个视频&#xff0c;让我参考着做。我看着视频上的地图旋转了方向、地图有标记、看着像是3D的&#xff08;视频上的地图使用多个图层叠加起来、CSS样式做了旋转&…

Nginx网络服务——location规则与rewrite重写

Nginx网络服务——location规则与rewrite重写 一、Nginx中location与rewrite1.location与rewrite常用的正则表达式2. location与rewrite的联系和区别 二、location的匹配规则1.location 的匹配分类2.location 常用的匹配规则3.location 优先级4.location匹配规则优先通用的总结…

【知识图谱搭建到应用】--知识存储--04

文章目录 Mysqljenafuseki数据存储数据建模数据映射注意事项 py2neoneo4jPy2neo与Neo4j的版本问题Py2neo导入三元组数据批量导入csv文件 rdflib库 前面几篇在讲述骗理论的内容&#xff0c;本片主要描述如何将清洗过的结构化数据存储在转换成三元组并存储起来&#xff0c;并于后…

ChatGPT与软件架构(4) - 架构师提示工程指南

架构师可以通过各种类型的对话提示&#xff0c;提升驱动ChatGPT对话输出的质量&#xff0c;更好的利用AI能力辅助架构设计。原文: Software Architects’ Guide to Enhancing ChatGPT Interactions With Prompt Types Robert Stump Unsplash 前言 随着ChatGPT等人工智能语言模型…