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默认)
相关推荐
天天要nx10 分钟前
D64【python 接口自动化学习】- python基础之数据库
数据库·python
落落落sss25 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
我救我自己25 分钟前
UE5运行时创建slate窗口
java·服务器·ue5
精进攻城狮@44 分钟前
Redis(value的数据类型)
数据库·redis
2401_853275731 小时前
ArrayList 源码分析
java·开发语言
爪哇学长1 小时前
SQL 注入详解:原理、危害与防范措施
xml·java·数据库·sql·oracle
MoFe11 小时前
【.net core】【sqlsugar】字符串拼接+内容去重
java·开发语言·.netcore
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ1 小时前
mybatisPlus打印sql配置
数据库·sql
_江南一点雨1 小时前
SpringBoot 3.3.5 试用CRaC,启动速度提升3到10倍
java·spring boot·后端
转转技术团队1 小时前
空间换时间-将查询数据性能提升100倍的计数系统实践
java·后端·架构