目录
[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_ID 和 DB_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 做对比,判断是否可见。
判断流程如下:
-
如果
DB_TRX_ID < up_limit_id→ 表示这个版本是在当前活跃事务集合之前就已经提交的。
✅ 可见!
-
如果
DB_TRX_ID >= low_limit_id→ 表示这个版本是由"将来"的事务创建的(比当前最晚的未提交事务还晚)。
❌ 不可见!继续往前找旧版本。
-
如果
up_limit_id <= DB_TRX_ID < low_limit_id→ 这个版本可能是由当前活跃事务之一创建的,需进一步判断:
- 若
DB_TRX_ID在trx_ids列表中 → 此事务尚未提交 → ❌ 不可见 - 若
DB_TRX_ID不在trx_ids列表中 → 此事务已提交 → ✅ 可见
- 若
-
特殊情况:
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 的优势与局限
优势:
-
读不加锁,提升并发性能
读操作无需等待写操作释放锁,极大提升了系统的吞吐量。
-
自然支持可重复读
在 RR 隔离级别下,无需额外机制即可保证一致性读。
-
减少锁争用和死锁概率
大量读操作不再参与锁竞争,系统更稳定。
缺点:
-
空间开销大
Undo Log 需要长期保留未被 purge 的历史版本,占用磁盘空间。
-
Purge 机制复杂
需要有后台线程定期清理不再需要的历史版本(purge thread)。
-
不能完全避免幻读
虽然 MVCC 解决了"记录内容"的一致性,但无法阻止新插入的记录带来的"幻影行"问题,仍需依赖 Next-Key Lock 等机制。
总结
MVCC 的本质是空间换时间。
-
Undo Log 提供历史版本的物理存储。
-
隐藏字段 串联成逻辑版本链。
-
Read View 提供了判断可见性的算法规则。
通过这套机制,MySQL 在保证 ACID 特性的同时,极大地压榨了系统的并发处理能力。理解 MVCC,不仅能帮你应对面试,更能在实际开发中优化事务逻辑,避免数据库因长事务或死锁而崩溃。