深入理解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,不仅能帮你应对面试,更能在实际开发中优化事务逻辑,避免数据库因长事务或死锁而崩溃。

相关推荐
G皮T2 小时前
【Elasticsearch】查询性能调优(六):track_total_hits 影响返回结果的相关性排序吗
大数据·数据库·elasticsearch·搜索引擎·全文检索·性能·opensearch
ZePingPingZe2 小时前
静态代理、JDK和Cglib动态代理、回调
java·开发语言
夜光小兔纸2 小时前
Oracle 表新增 ID RAW(16) 字段并填充历史数据
数据库·sql·oracle
万粉变现经纪人2 小时前
如何解决 pip install 代理报错 SOCKS5 握手失败 ReadTimeoutError 问题
java·python·pycharm·beautifulsoup·bug·pandas·pip
风月歌2 小时前
2025-2026计算机毕业设计选题指导,java|springboot|ssm项目成品推荐
java·python·小程序·毕业设计·php·源码
heartbeat..2 小时前
Web 状态管理核心技术详解 + JWT 双 Token (Access/Refresh Token) 自动登录
java·网络·jwt·token
Seven972 小时前
剑指offer-57、二叉树的下一个节点
java
DYS_房东的猫2 小时前
Spring Boot集成华为云OBS实现文件上传与预览功能(含安全下载)
java·spring boot
寂寞恋上夜2 小时前
PRD权限矩阵怎么写:RBAC模型+5个真实案例
数据库·人工智能·矩阵·deepseek ai·markdown转xmind·ai思维导图生成器