事务03之MVCC机制

embedded/2025/2/2 4:33:09/

MVCC 多版本并发控制机制

文章目录

  • MVCC 多版本并发控制机制
    • 一:并发事务的场景
      • 1:读读场景
      • 2:写写场景
      • 3:读写 or 写读场景
    • 二:MVCC机制综述
      • 1:MVCC日常生活的体现
      • 2:多版本并发控制
    • 三:MVCC实现原理剖析(重点,难点)
      • 1:隐藏字段
        • 1.1:隐藏主键 - ROW_ID(6Bytes)
        • 1.2:删除标识 - Deleted_Bit(1Bytes)
        • 1.3:最近更新的事务ID - TRX_ID(6Bytes)
        • 1.4:回滚指针 - ROLL_PTR(7Bytes)
      • 2:undo_log日志
      • 3:readView(核心)
        • 3.1:到底什么是readView
        • 3.2:机制实现原理和可见性算法
        • 3.3:RC和RR的区别

一:并发事务的场景

1:读读场景

读-读场景即是指多个事务/线程在一起读取一个相同的数据,比如事务T1正在读取ID=88的行记录,事务T2也在读取这条记录,两个事务之间是并发执行的。
在这里插入图片描述
对于这种情况而言,不需要做任何操作,因为不改变数据就不会引起任何并发问题

2:写写场景

多个事务之间一起对同一数据进行写操作,比如事务T1对ID=88的行记录做修改操作,事务T2则对这条数据做删除操作

事务T1提交事务后想查询看一下,结果连这条数据都不见了,这也是所谓的脏写问题,也被称为更新覆盖问题

对于这个问题在所有数据库、所有隔离级别中都是零容忍的存在,最低的隔离级别也要解决这个问题。【写互斥就可以解决脏写问题】

在这里插入图片描述

3:读写 or 写读场景

读写 -> 先读后写

在这里插入图片描述

写读,先写后读

在这里插入图片描述
并发事务中同时存在读、写两类操作时,这是最容易出问题的场景,脏读、不可重复读、幻读都出自于这种场景中

当有一个事务在做写操作时,读的事务中就有可能出现这一系列问题,因此数据库才会引入各种机制解决。

二:MVCC机制综述

MVCC机制的全称为Multi-Version Concurrency Control,即多版本并发控制技术,主要是为了提升数据库并发性能而设计的

其中采用更好的方式处理了读-写并发冲突,做到即使有读写冲突时,也可以不加解决,从而确保了任何时刻的读操作都是非阻塞的。

1:MVCC日常生活的体现

文章的发布和审核

假设我发布了一篇关于xxx的文章,发布后有一位观看者比较细心,文中存在两三个错别字,被这人指出来了,因此我去修正错别字后重新发布。

问题来了,对于文章首次发布也好,重新发布也罢,绝对要等审核通过后才会正式发布的,那我修正文章后重新发布,文章又会进入「审核中」这个状态

此时对于其他正在看、准备看的人来说,文章是不是就不见了?毕竟文章还在审核,因此对这个业务需求又该如何实现呢?多版本!

也就是说,对于首次发布后通过审核的文章,在后续重新发布审核时,用户可以看到更新前的文章,也就是看到老版本的文章,当更新后的文章审核通过后,再使用新版本的文章代替老版本的文章即可。

这样就能做到新老版本的兼容,也能够确保文章修正时,其他正在阅读的小伙伴不会受影响,而MySQL-MVCC机制的思想也大致相同。
在这里插入图片描述

2:多版本并发控制

MySQL中的多版本并发控制,也和上面给出的例子类似

回想一下,脏读、不可重复读、幻读问题都是由于多个事务并发读写导致的,但这些问题都是基于最新版本的数据并发操作才会出现

如果读、写的事务操作的不是同一个版本呢?这样就可以做到互不影响

⚠️ MySQL中仅在RC、RR才会使用MVCC机制

  • 如果是RU允许存在脏读问题、允许一个事务读取另一个事务未提交的数据,那自然可以直接读最新版本的数据,因此无需MVCC介入。

  • 如果是Serializable串行化级别,因为会将所有的并发事务串行化处理,也就是不论事务是读操作,亦或是写操作,都会被排好队一个个执行,这都不存在所谓的多线程并发问题了,自然也无需MVCC介入。

🖊 MVCC机制在MySQL中,仅有InnoDB引擎支持,而在该引擎中,MVCC机制只对RC、RR两个隔离级别下的事务生效。

当然,RC、RR两个不同的隔离级别中,MVCC的实现也存在些许差异

三:MVCC实现原理剖析(重点,难点)

MVCC由三个部分构成:隐藏字段,undoLog, readview

1:隐藏字段

通常而言,当你基于InnoDB引擎建立一张表后,MySQL除开会构建你显式声明的字段外,通常还会构建一些InnoDB引擎的隐藏字段

在InnoDB引擎中主要有DB_ROW_IDDB_Deleted_BitDB_TRX_IDDB_ROLL_PTR这四个隐藏字段

在这里插入图片描述

1.1:隐藏主键 - ROW_ID(6Bytes)

对于InnoDB引擎的表而言,由于其表数据是按照聚簇索引的格式存储,因此通常都会选择主键作为聚簇索引列,然后基于主键字段构建索引树

但如若表中未定义主键,则会选择一个具备唯一非空属性的字段,作为聚簇索引的字段来构建树。

而当两者都不存在时,InnoDB就会隐式定义一个顺序递增的列ROW_ID来作为聚簇索引列。

因此要牢记一点,如果你选择的引擎是InnoDB,就算你的表中未定义主键、索引,其实默认也会存在一个聚簇索引

只不过这个索引在上层无法使用,仅提供给InnoDB构建树结构存储表数据。

1.2:删除标识 - Deleted_Bit(1Bytes)

对于一条delete语句而言,当执行后并不会立马删除表的数据,而是将这条数据的Deleted_Bit删除标识改为1/true

后续的查询SQL检索数据时,如果检索到了这条数据,但看到隐藏字段Deleted_Bit=1时,就知道该数据已经被其他事务delete了,因此不会将这条数据纳入结果集。

设计Deleted_Bit这个隐藏字段的好处是什么呢?

主要是能够有利于聚簇索引

比如当一个事务中删除一条数据后,后续又执行了回滚操作,假设此时是真正的删除了表数据,会发生什么情况呢?

  • ①删除表数据时,有可能会破坏索引树原本的结构,导致出现叶子节点合并的情况。
  • 事务回滚时,又需重新插入这条数据,再次插入时又会破坏前面的结构,导致叶子节点分裂。

综上所述,如果执行delete语句就删除真实的表数据,由于事务回滚的问题,就很有可能导致聚簇索引树发生两次结构调整,这其中的开销可想而知;而且先删除,再回滚,最终树又变成了原状,那这两次树的结构调整还是无意义的。

所以,当执行delete语句时,只会改变将隐藏字段中的删除标识改为1/true,如果后续事务出现回滚动作,直接将其标识再改回0/false即可

会不会产生过多的deleted = 1的数据使得磁盘爆满呢?

MySQL中存在purger线程的概念,为了防止“已删除”的数据占用过多的磁盘空间,purger线程会自动清理Deleted_Bit=1/true的行数据。

当然,为了确保清理数据时不会影响MVCC的正常工作,purger线程自身也会维护一个ReadView

如果某条数据的Deleted_Bit=true,并且TRX_ID对purger线程的ReadView可见,那么这条数据一定是可以被安全清除的(即不会影响MVCC工作)。

1.3:最近更新的事务ID - TRX_ID(6Bytes)

MySQL对于每一个创建的事务,都会为其分配一个事务ID[TRX_ID],事务ID同样遵循顺序递增的特性,即后来的事务ID绝对会比之前的ID要大,比如:

此时事务T1准备修改表字段的值,MySQL会为其分配一个事务ID=1,当事务T2准备向表中插入一条数据时,又会为这个事务分配一个ID=2…

但有一个细节点需要记住:MySQL对于所有包含写入SQL的事务,会为其分配一个顺序递增的事务ID,但如果是一条select查询语句,则分配的事务ID=0。

不过对于手动开启的事务,MySQL都会为其分配事务ID,就算这个手动开启的事务中仅有select操作。

表中的隐藏字段TRX_ID,记录的就是最近一次改动当前这条数据的事务ID,这个字段是实现MVCC机制的核心之一。

1.4:回滚指针 - ROLL_PTR(7Bytes)

ROLL_PTR全称为rollback_pointer,也就是回滚指针的意思,这个也是表中每条数据都会存在的一个隐藏字段

当一个事务对一条数据做了改动后,会将旧版本的数据放到Undo-log日志中,而rollback_pointer就是一个地址指针,指向Undo-log日志中旧版本的数据

当需要回滚事务时,就可以通过这个隐藏列,来找到改动之前的旧版本数据,而MVCC机制也利用这点,实现了行数据的多版本。

2:undo_log日志

Undo-log中并不仅仅只存储一条旧版本数据,其实在该日志中会有一个版本链

在这里插入图片描述

从上图中可明显看出:不同的旧版本数据,会以roll_ptr回滚指针作为链接点,然后将所有的旧版本数据组成一个单向链表。

⚠️ 最新的旧版本数据,都会插入到链表头中,而不是追加到链表尾部。[头插法]

上述update语句的详细过程

  1. 对要修改的行数据加上排他
  2. 将原本的旧数据拷贝到Undo-log的rollback Segment区域。
  3. 对表数据上的记录进行修改,修改完成后将隐藏字段中的trx_id改为当前事务ID。
  4. 将隐藏字段中的roll_ptr指向Undo-log中对应的旧数据,并在提交事务后释放

为什么Undo-log日志要设计出版本链呢?

  1. 一方面可以实现事务点回滚(这点回去参考事务篇)
  2. 另一方面则可以实现MVCC机制(这点后面聊)。

移除机制

与删除标识类似,Undo-log移除的工作同样由purger线程负责

purger线程内部也会维护一个ReadView,它会以此作为判断依据,来决定何时移除Undo记录。

3:readView(核心)

思考一个问题:如果T2事务要查询一条行数据,此时这条行数据正在被T1事务写,那也就代表着这条数据可能存在多个旧版本数据

T2事务在查询时,应该读这条数据的哪个版本呢?

在这里插入图片描述
此时就需要用到ReadView,用它来做多版本的并发控制,根据查询的时机来选择一个当前事务可见的旧版本数据读取。

3.1:到底什么是readView

先来说下两个概念,快照读和当前读:

  • 当前读 :在定读(使用隔离事物)的时候读到的是最新版本的数据

  • 快照读:可重复读下mvcc生效读取的是数据的快照,并不是最新版本的数据(未提交事物的数据)

  • ------- 可以这么区分 ------

  • 快照读:读取的是快照版本。普通的SELECT就是快照读。通过mvcc来进行并发控制的,不用加

  • 当前读:读取的是最新版本。UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。

MVCC基于当前MySQL的运行状态生成的快照,也被称之为读视图,即ReadView

在这个快照中记录着当前所有活跃事务的ID(活跃事务是指还在执行的事务,即未结束(提交/回滚)的事务)。

当一个事务启动后,首次执行select操作时,MVCC就会生成一个数据库当前的ReadView,通常而言,一个事务与一个ReadView属于一对一的关系(不同隔离级别下也会存在细微差异),ReadView一般包含四个核心内容:

  • creator_trx_id:代表创建当前这个ReadView的事务ID。
  • trx_ids:表示在生成当前ReadView时,系统内活跃的事务ID列表。
  • up_limit_id:活跃的事务列表中,最小的事务ID。
  • low_limit_id:表示在生成当前ReadView时,系统中要给下一个事务分配的ID值。

⚠️ 值得一提的是low_limit_id,它并不是目前系统中活跃事务的最大ID,因为MySQL的事务ID是按序递增的,因此当启动一个新的事务时,都会为其分配事务ID,而这个low_limit_id则是整个MySQL中,要为下一个事务分配的ID值

在这里插入图片描述
假设目前数据库中共有T1~T5这五个事务,T1、T2、T4还在执行,T3已经回滚,T5已经提交

此时当有一条查询语句执行时,就会利用MVCC机制生成一个ReadView

由于单纯由一条select语句组成的事务并不会分配事务ID,因此默认为0,所以目前这个快照的信息如下:

{"creator_trx_id" : "0","trx_ids" : "[1,2,4]","up_limit_id" : "1","low_limit_id" : "6"
}
3.2:机制实现原理和可见性算法

经过前面得知:

  • 当一个事务尝试改动某条数据时,会将原本表中的旧数据放入Undo-log日志中。
  • 当一个事务尝试查询某条数据时,MVCC会生成一个ReadView快照。

其中Undo-log主要实现数据的多版本,ReadView则主要实现多版本的并发控制

假设现在有两个事务

# 原始数据如下
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+# 事务1 trx_id = 1
UPDATE zz_users SET user_name = "竹子" WHERE user_id = 1;
UPDATE zz_users SET user_sex = "男" WHERE user_id = 1;# 事务2 trx_id = 2
SELECT * FROM zz_users WHERE user_id = 1;

目前存在T1、T2两个并发事务,T1目前在修改ID=1的这条数据,而T2则准备查询这条数据,下面就是T2执行的具体的过程:

在这里插入图片描述
在这里插入图片描述
说简单一点,就是首先会去获取表中行数据的隐藏列,然后经过上述一系列判断后,可以得知:目前查询数据的事务到底能不能访问最新版的数据

  • 如果能,就直接拿到表中的数据并返回
  • 不能则去Undo-log日志中获取旧版本的数据返回。

该获取哪个版本的旧数据呢?

如果Undo-log日志中的旧数据存在一个版本链时,此时会首先根据隐藏列roll_ptr找到链表头,然后依次遍历整个列表,从而检索到最合适的一条数据并返回。合适的条件如下:旧版本的数据,其隐藏列trx_id不能在ReadView.trx_ids活跃事务列表中。

因为如果旧版本的数据,其trx_id依旧在ReadView.trx_ids中,就代表着产生这条旧数据的事务还未提交,自然不能读取这个版本的数据

范围查询时,突然出现新增数据怎么办呢?

SELECT * FROM zz_users;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
|       2 | 竹子      || 1234     | 2022-09-14 16:17:44 |
|       3 | 子竹      || 4321     | 2022-09-16 07:42:21 |
|       4 | 猫熊      || 8888     | 2022-09-27 17:22:59 |
|       9 | 黑竹      || 9999     | 2022-09-28 22:31:44 |
+---------+-----------+----------+----------+---------------------+-- T1事务:查询ID >= 3 的所有用户信息
select * from  zz_users where user_id >= 3;-- T2事务:新增一条 ID = 6 的用户记录
INSERT INTO zz_users VALUES(6,"棕熊","男","7777","2022-10-02 16:21:33");

此时当T1事务查询数据时,突然蹦出来一条ID=6的数据

经过判断之后会发现新增这条数据的事务还在执行,所以要去查询旧版本数据

但此时由于是新增操作,因此roll_ptr=null,即表示没有旧版本数据,此时不会读取最新版的数据

因为如果查询数据的事务不能读取最新版数据,同时又无法从版本链中找到旧数据,那就意味着这条数据对T1事务完全不可见,因此T1的查询结果中不会包含ID=6的这条新增记录。

3.3:RC和RR的区别

在这里插入图片描述

RC工作原理

在这里插入图片描述

RR工作原理

在这里插入图片描述


http://www.ppmy.cn/embedded/158807.html

相关文章

C++初阶 -- 初识STL和string类详细使用接口的教程(万字大章)

目录 一、STL 1.1 什么是STL 1.2 STL的版本 1.3 STL的六大组件 二、string类 2.1 string类的基本介绍 2.2 string类的默认成员函数 2.2.1 构造函数 2.2.2 析构函数 2.2.3 赋值运算符重载 2.3 string类对象的容量操作 2.3.1 size和length 2.3.2 capacity 2.3.3 r…

Contrastive Imitation Learning

机器人模仿学习中对比解码的一致性采样 摘要 本文中,我们在机器人应用的对比模仿学习中,利用一致性采样来挖掘演示质量中的样本间关系。通过在排序后的演示对比解码过程中,引入相邻样本间的一致性机制,我们旨在改进用于机器人学习…

计算机网络 IP 网络层 2 (重置版)

IP的简介: IP 地址是互联网协议地址(Internet Protocol Address)的简称,是分配给连接到互联网的设备的唯一标识符,用于在网络中定位和通信。 IP编制的历史阶段: 1,分类的IP地址: …

在 Windows 系统上,将 Ubuntu 从 C 盘 迁移到 D 盘

在 Windows 系统上,如果你使用的是 WSL(Windows Subsystem for Linux)并安装了 Ubuntu,你可以将 Ubuntu 从 C 盘 迁移到 D 盘。迁移过程涉及导出当前的 Ubuntu 发行版,然后将其导入到 D 盘的目标目录。以下是详细的步骤…

微信小程序date picker的一些说明

微信小程序的picker是一个功能强大的组件&#xff0c;它可以是一个普通选择器&#xff0c;也可以是多项选择器&#xff0c;也可以是时间、日期、省市区选择器。 官方文档在这里 这里讲一下date picker的用法。 <view class"section"><view class"se…

一文介绍Hive数据类型

一文介绍Hive数据类型 文章目录 一文介绍Hive数据类型写在前面基本数据类型集合数据类型介绍案例实操 类型转化隐式类型转换CAST操作 写在前面 Linux版本&#xff1a;CentOS7.5Hive版本&#xff1a;Hive-3.1.2 基本数据类型 如下表所示&#xff1a; Hive数据类型Java数据类型…

OVS-DPDK

dpdk介绍及应用 DPDK介绍 DPDK&#xff08;Data Plane Development Kit&#xff09;是一组快速处理数据包的开发平台及接口。有intel主导开发&#xff0c;主要基于Linux系统&#xff0c;用于快速数据包处理的函 数库与驱动集合&#xff0c;可以极大提高数据处理性能和吞吐量&…

被裁与人生的意义--春节随想

还有两个月就要被迫离开工作了十多年的公司了&#xff0c;不过有幸安安稳稳的过了一个春节&#xff0c;很知足! 我是最后一批要离开的&#xff0c;一百多号同事都没“活到”蛇年。看着一批批仁人志士被“秋后斩首”&#xff0c;马上轮到我们十来个&#xff0c;个中滋味很难言清…