MVCC详解

一. 描述

多版本并发控制:读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据,版本链.

MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据

行。而 SERIALIZABLE 则会对所有读取的行都加锁。

二. MVCC机制

MVCC机制主要通过隐藏字段Undo-log日志ReadView这三个东西实现的,因而这三玩意儿也被称为"MVCC三剑客"!

2.1 隐藏字段

  1. 事物id(trx_id):
    用来存储每次对某条聚簇索引记录进行修改的时候的事务id。
    表中每条数据都会存在的一个隐藏字段,当一个事务对一条数据做了改动后,都会将旧版本的数据放到Undo-log日志中,而rollback_pointer就是一个地址指针,指向Undo-log日志中旧版本的数据,当需要回滚事务时,就可以通过这个隐藏列,来找到改动之前的旧版本数据,而MVCC机制也利用这点,实现了行数据的多版本。
  2. 回滚指针(roll_pointer)
    每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个 roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)
  3. 隐藏主键(ROW_ID)
    对于InnoDB引擎的表而言,由于其表数据是按照聚簇索引的格式存储,因此通常都会选择主键作为聚簇索引列,然后基于主键字段构建索引树,但如若表中未定义主键,则会选择一个具备唯一非空属性的字段,作为聚簇索引的字段来构建树
  4. 删除标识(Deleted_Bit)
    对于一条delete语句而言,当执行后并不会立马删除表的数据,而是将这条数据的Deleted_Bit删除标识改为1/true,后续的查询SQL检索数据时,如果检索到了这条数据,但看到隐藏字段Deleted_Bit=1时,就知道该数据已经被其他事务delete了,因此不会将这条数据纳入结果集。

基于InnoDB引擎,在本次MVCC分析中,只关注事物id (trx_id)和 回滚指针(roll_pointer)两个隐藏列。

2.2 undo-log日志

MySQL事务机制是基于Undo-log实现的,Undo-log日志中会存储旧版本的数据,但要注意:Undo-log中并不仅仅只存储一条旧版本数据,其实在该日志中会有一个版本链

不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。

2.3 ReadView

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

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

上面四个值很简单,low_limit_id 并不是目前系统中活跃事务的最大ID,因为MySQL的事务ID是按序递增的,因此当启动一个新的事务时,都会为其分配事务ID,而这个low_limit_id则是整个MySQL中,要为下一个事务分配的ID值。

下面上个ReadView的示意图,来好好理解一下它:

三. MVCC实现原理

3.1 实现原理

①当一个事务尝试改动某条数据时,会将原本表中的旧数据放入Undo-log日志中。

②当一个事务尝试查询某条数据时,MVCC会生成一个ReadView快照

Undo-log主要实现数据的多版本,ReadView则主要实现多版本的并发控制,还是以之前的例子来举例说明:

sql 复制代码
-- 事务T1:trx_id=1
UPDATE `users` SET user_name = "煎饼狗子" WHERE user_id = 1;
UPDATE `users` SET user_sex = "男" WHERE user_id = 1;
sql 复制代码
-- 事务T2:trx_id=2
SELECT * FROM `users` WHERE user_id = 1;

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

①当事务中出现select语句时,会先根据MySQL的当前情况生成一个ReadView。

②判断行数据中的隐藏列trx_id与ReadView.creator_trx_id是否相同:

相同:代表创建ReadView和修改行数据的事务是同一个,自然可以读取最新版数据。

不相同:代表目前要查询的数据,是被其他事务修改过的,继续往下执行。
③判断隐藏列trx_id是否小于ReadView.up_limit_id最小活跃事务ID:

小于:代表改动行数据的事务在创建快照前就已结束,可以读取最新版本的数据。

不小于:则代表改动行数据的事务还在执行,因此需要继续往下判断。
④判断隐藏列trx_id是否小于ReadView.low_limit_id这个值:

大于或等于:代表改动行数据的事务是生成快照后才开启的,因此不能访问最新版数据。

小于:表示改动行数据的事务ID在up_limit_id、low_limit_id之间,需要进一步判断。
⑤如果隐藏列trx_id小于low_limit_id,继续判断trx_id是否在trx_ids中:

在:表示改动行数据的事务目前依旧在执行,不能访问最新版数据。

不在:表示改动行数据的事务已经结束,可以访问最新版的数据。

3.2 版本链数据的访问规则(总结)

条件 是否可以访问 说明
trx_id == creator_trx_id 可以访问该版本 说明数据是当前这个事物更改的
trx_id < up_limit_id (最小活跃事物id) 可以访问该版本 说明数据已经提交了
trx_id > low_limit_id (预分配事物id) 不可以访问该版本 说明该事务是在ReadView生成后才开启
up_limit_id <= trx_id <= low_limit_id trx_id不在trx_ids中,是可以访问该版本 说明数据已经提交

3.3 RC,RR级别下MVCC机制

已提交读(RC)和可重复读(RR)的区别就在于它们生成ReadView的策略不同。

  • 已提交读(RC) : 每次select都会生成一个最新的 ReadView
  • 可重复读(RR) : 沿用第一次查询的 ReadView (mysql默认)
相关推荐
spencer_tseng2 小时前
Stream not available [SysDictDataMapper.xml]
xml·java
蒸蒸yyyyzwd7 小时前
cpp对象模型学习笔记1.1-2.8
java·笔记·学习
银发控、7 小时前
MySQL联合索引
数据库·mysql
予枫的编程笔记7 小时前
【MySQL修炼篇】从踩坑到精通:事务隔离级别的3大异常(脏读/幻读/不可重复读)解决方案
数据库·mysql·后端开发·数据库事务·事务隔离级别·rr级别·脏读幻读不可重复读
程序员徐师兄7 小时前
Windows JDK11 下载安装教程,适合新手
java·windows·jdk11 下载安装·jdk11 下载教程
RANCE_atttackkk8 小时前
[Java]实现使用邮箱找回密码的功能
java·开发语言·前端·spring boot·intellij-idea·idea
五岳8 小时前
DTS按业务场景批量迁移阿里云MySQL表实战(下):迁移管理平台设计与实现
java·应用·dts
一起养小猫8 小时前
Flutter for OpenHarmony 实战:记账应用数据统计与可视化
开发语言·jvm·数据库·flutter·信息可视化·harmonyos
世界尽头与你9 小时前
(修复方案)CVE-2023-22047: Oracle PeopleSoft Enterprise PeopleTools 未授权访问漏洞
数据库·安全·oracle·渗透测试
韩立学长9 小时前
【开题答辩实录分享】以《智能大学宿舍管理系统的设计与实现》为例进行选题答辩实录分享
数据库·spring boot·后端