【MySQL】MVCC与Read View

目录

一、数据库并发的三种场景

二、读写场景的MVCC

(一)表中的三个隐藏字段

[(二)undo 日志](#(二)undo 日志)

(三)模拟MVCC

[(四)Read View](#(四)Read View)

(五)当前读和快照读

三、RC和RR隔离级别的区别


一、数据库并发的三种场景

数据库作为存储大量数据的介质,一定存在着大量的IO操作,也就是写操作和读操作。

读-读 :不存在任何问题,也不需要并发控制;

读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读;

写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失。

本文主要讨论在读写场景下的并发问题。

二、读写场景的MVCC

MVCC即多版本并发控制协议,其是 InnoDB 存储引擎为了实现高并发事务处理的核心机制。它通过维护数据的多个版本,使读写操作可以非阻塞并行执行,从而极大提升了数据库的并发性能。

(一)表中的三个隐藏字段

当新建一个表结构时,除了显示定义的列结构,表中还包含了三个隐藏字段:

  • DB_TRX_ID:6字节,记录最近修改本条记录的事务ID;
  • DB_ROLL_PTR:7字节,回滚指针,记录该条记录的上一版本;
  • DB_ROW_ID:6字节,当数据表没有主键时,InnoDB会自动以 DB_ROW_ID 作为隐藏主键并建立一个聚簇索引。

例如向一张表插入一条数据时,实际该数据内容为:

|----------|---------|---------------|--------------------|---------------|
| name | age | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
| 张三 | 18 | 最近修改本条记录的事务ID | null(新增数据因此没有上个版本) | 1(隐藏主键) |

实际数据表还有个删除 flag 隐藏字段,用于表明该条是否有效,也就是删除表中数据时是逻辑删除,之后在合适的时候由 MySQL 再向磁盘刷新数据。

(二)undo 日志

undo 日志是在 MySQL 中的一段内存缓冲区,用于保存日志文件。其主要由两个核心作用:

  • 事务回滚:
    当事务执行失败或者主动回滚时,undo log 中记录的数据旧版本课用于恢复原始状态。例如:当执行插入数据操作时,undo log 会记录其对应的删除操作,回滚时直接执行该操作删除数据;当执行更新或删除操作时,undo log会记录数据的旧值用于回滚。
    事务在修改数据前,undo log会记录反向操作或数据旧值,形成逻辑日志链。
  • 支持MVCC:
    undo log会存储数据的历史版本,通过隐藏字段 DB_TRX_ID 和 DB_ROLL_PTR串联历史版本,形成链式日志从而支持 MVCC 。详见下文。

(三)模拟MVCC

假定 student 表中已有数据如下:

假定有个事务10,对 student 表中记录进行了修改,将姓名修改为了"李四"。当事务10执行完毕后:

在此过程中,事务10首先会将该条记录加行锁,修改前先将该条记录拷贝到 undo log 中(写时拷贝)。之后将数据修改为目标值并再填写相应的字段,之后事务提交后并释放锁。

假定现在有个事务11对表中数据进行修改,将年龄改为了20,事务11也会进行以上的操作。当事务11执行完毕后:

undo log中的一个个版本被称为快照。正如上文所述,除了记录版本链以外,undo log 还会记录相反的操作以备回滚。

当执行插入操作时,undo log 会基于主键记录对应相反的删除操作;当执行删除操作时会将该记录的删除 flag 字段设置为删除即逻辑删除,并将该条数据记录在 undo log 中;当执行select 操作时,会根据隔离级别执行当前读 或者快照读。当前读即读取最新的数据,快照读即读取数据的历史版本。

针对于 select 操作,在RU隔离级别下所有查询都是读取最新版本的数据,RC和RR隔离级别下所有普通查询都是快照读,而Serializable隔离级别下事务是严格串行执行,因此所有查询操作都是当前读。本文主要讨论如何MVCC 机制如何解决 RC 和 RR隔离级别下的读写并发问题。

隔离级别和读写并发问题详见:【MySQL】事务及隔离性-CSDN博客

(四)Read View

Read View 是事务首次进行快照读时由 MySQL 生成的,其主要是配合 MVCC 机制进行版本控制。

当某个事务执行 select 快照读的时候,MySQL新建一个 Read View 对象,用其内部的字段来判断当前事务应该读取数据的哪个版本,该数据可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据,这由隔离级别决定。

以下是简化 Read View 的结构体:

cpp 复制代码
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;//ids_t集合类型 
    /** 配合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

up_limit_id:记录m_ids列表中事务ID最小的ID

low_limit_id:ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1

creator_trx_id:创建该ReadView的事务ID

表中的三个隐藏字段配合 Read View 即可完成 MVCC机制。

当一个事务进行快照读时,MySQL 会为此建立一个 Read View 对象,首先 up_limit_id 字段会记录当前活跃事务的最小事务ID,low_limit_id 会记录当前活跃时最大事务ID值 + 1, 而 m_ids 会记录当前所有活跃的事务ID。例如:当事务10创建 Read View 对象时,假定活跃事务有(8,9,12),那么事务是10对应的字段值分别为:up_limit_id = 8, low_limit_id = 13, m_ids = (8, 9, 12)。

下面将展开说明 Read View 是如何配合 undo log 实现 MVCC机制的:

当一个新事务执行普通 select 操作(快照读)时,MySQL 会为此新建并初始化一个 Read View 对象,针对目标数据存在以下的情况:

若该条记录的 DB_TRX_ID 小于 Read View 中的 up_limit_id 最小事务ID,说明修改该记录的事务在新事务到来之前就已经执行完毕提交了,故该数据可被新事物所见,无需查看该条记录的上一版本了;

若该条记录的 DB_TRX_ID 大于等于 Read View 中的 low_limit_id 最大事务ID,说明修改该记录的事务在新事务执行查询操作之后才执行完毕(不一定提交),若该条记录存在上一版本,则需通过该条记录的 DB_ROLL_PTR 字段查询上一版本并再次进行比较;若不存在上一版本,则该条记录不可被新事务所见;

若该条记录的 DB_TRX_ID 处于 up_limit_id 与 low_limit_id 之间,则需要进一步判断。若该条记录的 DB_TRX_ID 存在于 m_ids 列表中,则说明新事务执行查询操作时 DB_TRX_ID 该事务仍处于活跃状态,因此该条记录不可见,需查询该记录上一版本进一步进行判断;若该条记录的 DB_TRX_ID 不存在于 m_ids 列表中,则说明新事务执行查询操作时 DB_TRX_ID 该事务已经执行完毕提交了,则该条记录可以被新事务所见,无需查看该条记录的上一版本了。

(五)当前读和快照读

在上文中我们铺垫了当前读和快照读的概念,那么应该如何操作呢?以下示例都是在 RR 隔离级别下进行测试。

两个事务同时开启,由上面两张图可知,MySQL会为快照读建立 Read View 对象,因此不同的读取可能会造成不同的查询结果。(图一是修改年龄为20,但查询结果为18/图二是修改年龄为18,查询结果也为18)

当前读与快照读:

sql 复制代码
//当前读
mysql> select * from student lock in share mode;
//快照读
mysql> select * from student;

三、RC和RR隔离级别的区别

RC和RR隔离级别下在快照读时都会生成 Read View 对象**,正是生成 Read View 对象的时机不同,导致快照读的结果不同**。

在RC隔离级别下:

每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因,也就是每个快照读都会生成最新的 Read View 对象。正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题

在RR隔离级别下:

同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。也就是只有第一次进行快照读时才会生成 Read View 对象,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可 见;

相关推荐
IvorySQL4 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·4 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德4 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫4 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i4 小时前
完全卸载MariaDB
数据库·mariadb
纤纡.4 小时前
Linux中SQL 从基础到进阶:五大分类详解与表结构操作(ALTER/DROP)全攻略
linux·数据库·sql
jiunian_cn5 小时前
【Redis】渐进式遍历
数据库·redis·缓存
橙露5 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot
冰暮流星5 小时前
sql语言之分组语句group by
java·数据库·sql
符哥20085 小时前
Ubuntu 常用指令集大全(附实操实例)
数据库·ubuntu·postgresql