深入理解MySQL中的MVCC:多版本并发控制的实现原理

目录

[1、 MVCC 核心定义:读不阻塞写](#1、 MVCC 核心定义:读不阻塞写)

[1.1 Multi-Version Concurrency Control](#1.1 Multi-Version Concurrency Control)

[1.2 为什么需要 MVCC?](#1.2 为什么需要 MVCC?)

[1.3 核心概念一:快照读 vs 当前读](#1.3 核心概念一:快照读 vs 当前读)

[1.3.1 快照读(Snapshot Read)](#1.3.1 快照读(Snapshot Read))

[1.3.2 当前读(Current Read)](#1.3.2 当前读(Current Read))

[2. 核心组件一:Undo Log ------ 快照的存储之地](#2. 核心组件一:Undo Log —— 快照的存储之地)

[2.1 什么是 Undo Log?](#2.1 什么是 Undo Log?)

[3. 核心组件二:隐式字段 ------ 构建快照链的关键](#3. 核心组件二:隐式字段 —— 构建快照链的关键)

[3.1 三大隐式字段详解:](#3.1 三大隐式字段详解:)

[4. 核心机制:Read View ------ 决定"你能看到谁"](#4. 核心机制:Read View —— 决定“你能看到谁”)

[4.1 什么是 Read View?](#4.1 什么是 Read View?)

[4.2 可见性判断算法:如何决定读哪个版本?](#4.2 可见性判断算法:如何决定读哪个版本?)

[5. MVCC 与事务隔离级别](#5. MVCC 与事务隔离级别)

[5.1 不用事务隔离级别对应的Read View 创建时机](#5.1 不用事务隔离级别对应的Read View 创建时机)

[5.2 关键区别:Read View 的创建策略](#5.2 关键区别:Read View 的创建策略)

[5.3 MVCC 的优势与局限](#5.3 MVCC 的优势与局限)

总结


在高并发系统中,数据库的并发控制是保障数据一致性和性能的关键。传统的"加锁"机制(Locking)虽能解决问题,但阻塞和死锁往往成为性能瓶颈。

InnoDB 存储引擎引入了 MVCC (Multiversion Concurrency Control),即多版本并发控制。它让数据库在不加锁的情况下,实现了高效的读写并发。本文将带你从底层原理到逻辑算法,全面拆解 MVCC 的运行机制。

1、 MVCC 核心定义:读不阻塞写

1.1 Multi-Version Concurrency Control

MVCC,全称 Multi-Version Concurrency Control,翻译为"多版本并发控制"。它是一种用于提高数据库并发性能的技术,其核心思想是:

每条记录可以有多个历史版本,不同的事务根据自己的视角看到不同版本的数据,从而实现读不阻塞写、写不阻塞读的效果。

  • 核心思想:每条记录在被修改时,旧版本不会被立即覆盖,而是被保存在 Undo Log 中。不同的事务根据自己的"快照视图"读取对应的版本。

  • 最终效果读不阻塞写,写不阻塞读。仅在"写-写"冲突时才需要通过锁机制来排队。

这听起来有点像"时间机器"------每个事务都能看到一个属于自己的"过去时刻"的数据快照,而不会被其他事务的修改所干扰。

1.2 为什么需要 MVCC?

在并发场景下,数据库操作主要分为两类:读(Read)写(Write),由此产生三种典型的并发情况:

并发类型 是否存在问题 常见解决方案
读-读并发 ❌ 不会 无需特殊处理
读-写并发 ✅ 可能 MVCC / 共享锁
写-写并发 ✅ 必须处理 排他锁 / 悲观锁

其中:

  • 读-读并发:多个事务同时读取数据,天然无冲突。
  • 写-写并发:必须互斥,通常使用加锁解决(如行锁)。
  • 读-写并发 :传统方式是让读等待写完成(共享锁),但这严重影响性能。MVCC 正是用来优雅地解决这个问题的利器

1.3 核心概念一:快照读 vs 当前读

要理解 MVCC,首先要搞清楚两个关键概念:快照读(Snapshot Read)当前读(Current Read)

1.3.1 快照读(Snapshot Read)

快照读是指读取的是某个时间点生成的数据快照,而不是最新的数据。这种读操作不需要加锁,因此不会阻塞写操作。

sql 复制代码
-- 典型的快照读语句
SELECT * FROM users WHERE id = 1;

这类普通的 SELECT 查询,在没有显式加锁的情况下,就是快照读。它们读取的是基于事务开始时或第一次查询时建立的"一致性视图"。

快照读是 MVCC 实现的基础。

1.3.2 当前读(Current Read)

当前读则是读取数据的最新版本,并且通常伴随着加锁行为,以确保读到的是已提交的最新数据。

以下都是当前读的例子:

sql 复制代码
-- 显式加锁的读
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
SELECT * FROM users WHERE id = 1 FOR UPDATE;

-- 所有写操作都会进行当前读
INSERT INTO users VALUES (...);
UPDATE users SET name = 'Tom' WHERE id = 1;
DELETE FROM users WHERE id = 1;

2. 核心组件一:Undo Log ------ 快照的存储之地

既然快照读能读到"过去"的数据,那这些"旧版本"数据存在哪里呢?答案就是:Undo Log

2.1 什么是 Undo Log?

Undo Log 是 InnoDB 中一种特殊的事务日志,主要用于:

  • 支持事务回滚(Rollback)
  • 提供多版本数据支持(MVCC)
  • 数据库崩溃恢复

每当一条记录被更新(UPDATE)、删除(DELETE)之前,InnoDB 都会先将该记录的旧值保存到 Undo Log 中。这个过程就像是给数据拍了一张"照片",然后把照片存进一个叫"时光胶囊"的地方。

举个例子:

复制代码
-- 初始状态:name = 'Alice'
UPDATE users SET name = 'Bob' WHERE id = 1;

执行这条语句前,InnoDB 会把 (id=1, name='Alice') 这条原始记录写入 Undo Log,然后再修改聚簇索引上的数据为 'Bob'

这样,即使数据已经被改成了 'Bob',我们依然可以通过 Undo Log 找回 'Alice' 的版本。

📌 所以,Undo Log 就是 MVCC 中所有历史快照的存放地!


3. 核心组件二:隐式字段 ------ 构建快照链的关键

InnoDB 在每一行记录中,除了用户定义的字段之外,还会自动维护几个隐藏字段,它们对 MVCC 至关重要。

3.1 三大隐式字段详解:

字段名 含义说明
DB_ROW_ID 隐藏主键。如果你没有定义主键,InnoDB 会自动生成一个递增的 row_id 作为聚簇索引键。
DB_TRX_ID 最近一次修改该记录的事务 ID。每次更新都会更新此值。
DB_ROLL_PTR 回滚指针,指向该记录上一个版本在 Undo Log 中的位置。

这三个字段中,DB_TRX_IDDB_ROLL_PTR 是 MVCC 的灵魂所在

当某条记录被多次修改时,就会形成一条由 Undo Log 组成的"版本链":

复制代码
[最新版本] → DB_TRX_ID=50 → DB_ROLL_PTR → [旧版本4] (trx_id=40)
                                     ↓
                               [旧版本3] (trx_id=30)
                                     ↓
                               [旧版本2] (trx_id=20)
                                     ↓
                               [初始版本] (trx_id=10)

每一个旧版本都通过 DB_ROLL_PTR 指向上一个版本,构成一个逆序的链表结构。

这就是所谓的"版本链"或"快照链"。

当我们需要进行快照读时,就可以顺着这条链一路往上找,直到找到一个对当前事务可见的版本为止。


4. 核心机制:Read View ------ 决定"你能看到谁"

有了 Undo Log 和版本链,我们已经可以获取历史数据了。但问题来了:

在一个并发环境中,我这个事务到底应该看到哪个版本的数据?

这就轮到 Read View 登场了!

4.1 什么是 Read View?

Read View 是在事务执行快照读时创建的一个"一致性视图",用来判断哪些数据版本对当前事务是可见的,哪些是不可见的。

它本质上是一个快照读发生时,数据库当前活跃事务的状态快照。

Read View 包含四个关键属性:

属性名 含义
trx_ids 当前系统中所有未提交事务的事务 ID 列表(按创建顺序排列)。
low_limit_id trx_ids 中最大的事务 ID + 1,即下一个可能分配的事务 ID。也可以理解为当前未提交事务中的上限。
up_limit_id trx_ids 中最小的事务 ID,表示最早还未提交的事务。
creator_trx_id 创建该 Read View 的事务自身的 ID。如果是只读事务,则为 0。

💡 注意:事务 ID 是全局自增的,ID 越小表示事务越早启动。

4.2 可见性判断算法:如何决定读哪个版本?

当我们要读取某一行数据时,InnoDB 会从最新版本开始,沿着版本链逐个检查每个版本的 DB_TRX_ID,并与当前 Read View 做对比,判断是否可见。

判断流程如下:

  1. 如果 DB_TRX_ID < up_limit_id

    → 表示这个版本是在当前活跃事务集合之前就已经提交的。

    可见!

  2. 如果 DB_TRX_ID >= low_limit_id

    → 表示这个版本是由"将来"的事务创建的(比当前最晚的未提交事务还晚)。

    不可见!继续往前找旧版本。

  3. 如果 up_limit_id <= DB_TRX_ID < low_limit_id

    → 这个版本可能是由当前活跃事务之一创建的,需进一步判断:

    • DB_TRX_IDtrx_ids 列表中 → 此事务尚未提交 → ❌ 不可见
    • DB_TRX_ID 不在 trx_ids 列表中 → 此事务已提交 → ✅ 可见
  4. 特殊情况:DB_TRX_ID == creator_trx_id

    → 即使该事务仍在列表中,也认为可见,因为这是自己修改的数据。

总结一句话:只有那些在当前 Read View 创建前已经提交的事务所做的修改,才是可见的。

5. MVCC 与事务隔离级别

5.1 不用事务隔离级别对应的Read View 创建时机

MVCC 并不是孤立存在的,它的行为会受到 事务隔离级别(Isolation Level) 的影响。

在 MySQL 的 InnoDB 引擎中,MVCC 主要在 READ COMMITTED(RC)REPEATABLE READ(RR) 这两个隔离级别下发挥作用。

隔离级别 Read View 创建时机 是否解决不可重复读 是否解决幻读
READ UNCOMMITTED ------
READ COMMITTED (RC) 每次快照读都新建 ❌(部分)
REPEATABLE READ (RR) 第一次快照读时创建,后续复用 ✅✅ ✅(配合间隙锁)
SERIALIZABLE 加锁串行化 ✅✅

5.2 关键区别:Read View 的创建策略

在 REPEATABLE READ 下:

  • 只在事务中第一次快照读时创建 Read View
  • 后续所有的快照读都复用同一个 Read View
  • 因此,整个事务期间看到的数据版本是一致的

完美解决了"不可重复读"问题!

举例:你在事务中两次执行 SELECT * FROM users WHERE id=1,结果完全一样,哪怕别人在这期间修改并提交了数据。

在 READ COMMITTED 下:

  • 每次执行快照读都会重新生成一个新的 Read View
  • 所以每次都能看到最新已提交的数据

解决了脏读,但可能出现"不可重复读"。

第一次查是 'Alice',第二次查变成了 'Bob',因为中间有人提交了更新。

5.3 MVCC 的优势与局限

优势:

  1. 读不加锁,提升并发性能

    读操作无需等待写操作释放锁,极大提升了系统的吞吐量。

  2. 自然支持可重复读

    在 RR 隔离级别下,无需额外机制即可保证一致性读。

  3. 减少锁争用和死锁概率

    大量读操作不再参与锁竞争,系统更稳定。

缺点:

  1. 空间开销大

    Undo Log 需要长期保留未被 purge 的历史版本,占用磁盘空间。

  2. Purge 机制复杂

    需要有后台线程定期清理不再需要的历史版本(purge thread)。

  3. 不能完全避免幻读

    虽然 MVCC 解决了"记录内容"的一致性,但无法阻止新插入的记录带来的"幻影行"问题,仍需依赖 Next-Key Lock 等机制。

总结

MVCC 的本质是空间换时间

  1. Undo Log 提供历史版本的物理存储。

  2. 隐藏字段 串联成逻辑版本链。

  3. Read View 提供了判断可见性的算法规则。

通过这套机制,MySQL 在保证 ACID 特性的同时,极大地压榨了系统的并发处理能力。理解 MVCC,不仅能帮你应对面试,更能在实际开发中优化事务逻辑,避免数据库因长事务或死锁而崩溃。

相关推荐
努力也学不会java17 小时前
【Spring Cloud】环境和工程基本搭建
java·人工智能·后端·spring·spring cloud·容器
PuppyCoding17 小时前
EasyExcel 导出排除基类字段,不给基类加@ExcelIgnore 的方式。
java·开发语言
洲星河ZXH17 小时前
Java,泛型
java·开发语言·windows
海南java第二人17 小时前
SpringBoot循环依赖全解:从根源到解决方案的深度剖析
java·spring
CopyProfessor17 小时前
Java Agent 入门项目模板(含代码 + 配置 + 说明)
java·开发语言
duansamve17 小时前
VSCode中如何搭建JAVA+MAVEN开发环境?
java·vscode·maven
勇气要爆发17 小时前
向量数据库 Milvus 极速入门:从 Docker 部署到 Python 增删改查实战
数据库·docker·milvus
xuefuhe17 小时前
如何连接到postgresql数据库
数据库
Elias不吃糖17 小时前
Java Collection 体系与使用场景整理
java·学习笔记·map·collection
好好学操作系统17 小时前
notion+excel自动创建表格| 了解了notion api
数据库·python·oracle·excel·notion