Seal^_^头像
关注
读不加锁的魔法:MySQL 中的 MVCC 到底是什么?封面图

读不加锁的魔法:MySQL 中的 MVCC 到底是什么?


🌺The Begin🌺点点关注,收藏不迷路🌺

揭开 InnoDB 高并发读写的底层密码

1. 引言:数据库的读写矛盾

在高并发的数据库系统中,存在一个经典矛盾:

需求锁方案问题
读多写少读锁 + 写锁互斥读会阻塞写,写会阻塞读,并发度低
读写并发无锁方案可能读到未提交的脏数据

在这里插入图片描述

MVCC(Multi-Version Concurrency Control,多版本并发控制) 就是为了解决这个矛盾而生的技术——让读操作不加锁,写操作不阻塞读

本文将深度剖析 InnoDB 中 MVCC 的实现原理。

2. MVCC 的核心思想

2.1 一句话理解 MVCC

MVCC 的本质是:保存数据的多个历史版本,让不同事务看到不同版本的数据,从而实现读写不互斥。

事务视图

数据版本链

版本3: 余额=300
当前值

指针

版本2: 余额=200

指针

版本1: 余额=100

事务A
读版本1

事务B
读版本2

事务C
读版本3

2.2 MVCC 解决的问题

问题传统锁方案MVCC 方案
脏读加读锁读已提交版本
不可重复读加行锁读同一快照
幻读加间隙锁RR级别下用 Read View 抑制幻读
读写阻塞互斥阻塞完全不阻塞

2.3 InnoDB 中的实现范围

InnoDB 的 MVCC 仅实现在 READ COMMITTEDREPEATABLE READ 两个隔离级别下:

隔离级别MVCC 行为
READ UNCOMMITTED不启动 MVCC,直接读最新值(可能脏读)
READ COMMITTED每次查询生成新的 Read View(可读到已提交的新数据)
REPEATABLE READ首次查询生成 Read View,事务内不变(可重复读)
SERIALIZABLE退化为加锁读,MVCC 辅助

3. MVCC 的三大基石

InnoDB MVCC 的实现依赖于三个核心组件:

MVCC三大基石

隐藏列
DB_TRX_ID / DB_ROLL_PTR / DB_ROW_ID

Undo Log
版本链

Read View
一致性视图

判断行版本可见性

3.1 隐藏列:数据行自带版本号

InnoDB 为每行数据额外添加了三个隐藏列:

隐藏列大小作用
DB_TRX_ID6 字节最后修改该行的事务 ID
DB_ROLL_PTR7 字节指向 Undo Log 版本链的指针
DB_ROW_ID6 字节行 ID(表无主键时用于聚簇索引)

一行数据的物理结构

DB_TRX_ID
102

DB_ROLL_PTR
0x7F3A

DB_ROW_ID
1001

实际数据列
name='Alice', age=25

3.2 Undo Log:版本链

每次修改数据时,旧版本会写入 Undo Log,并通过 DB_ROLL_PTR 串联成版本链

Undo Log 版本链

当前版本

DB_ROLL_PTR

prev

prev

age=25
DB_TRX_ID=103
DB_ROLL_PTR

版本1: age=20
TRX_ID=102
prev指针

版本2: age=18
TRX_ID=101
prev指针

版本3: age=15
TRX_ID=100
prev=NULL

三条关键规则

  1. INSERT:创建新版本,DB_TRX_ID 为当前事务 ID
  2. UPDATE:将旧数据写入 Undo Log,更新数据行并修改 DB_TRX_ID
  3. DELETE:标记删除(逻辑删除),版本链中保留被删除的版本

3.3 Read View:事务的“快照镜头”

Read View 是事务发起查询时生成的一致性视图,记录了此刻数据库中所有活跃事务的状态。

渲染错误: Mermaid 渲染失败: Parse error on line 5: ... ACT[活跃事务ID列表
[101, 105, 108]] -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'SQS'
字段含义
low_limit_id当前未分配的最小事务 ID(新事务的起点)
up_limit_id当前活跃事务中最小的 ID
active_trx_ids当前所有活跃事务 ID 的集合

4. 可见性判断算法

当事务读取某行数据时,InnoDB 沿着版本链从新到旧遍历,找到第一个对当前事务可见的版本。

4.1 判断流程图

读取行版本
获取 DB_TRX_ID = trx_id

trx_id < up_limit_id?

✅ 可见
直接读取

trx_id >= low_limit_id?

❌ 不可见
沿 Undo 链找上一版本

trx_id 在
活跃事务列表中?

✅ 可见
已提交

trx_id = 当前事务ID?

✅ 可见
自己的修改

❌ 不可见
其他活跃事务未提交
沿 Undo 链找上一版本

返回数据

4.2 判断规则总结

条件可见性说明
trx_id < up_limit_id✅ 可见事务在 Read View 生成前已提交
trx_id >= low_limit_id❌ 不可见事务在 Read View 生成后开启
trx_id 在活跃列表中❌ 不可见事务未提交(且非当前事务)
trx_id = 当前事务ID✅ 可见自己的修改当然可见
trx_id 不在活跃列表且 < low_limit_id✅ 可见已提交的事务

5. RC vs RR:Read View 生成时机的差异

这是面试中最常被问到的问题——RC 和 RR 下 MVCC 的唯一区别

REPEATABLE_READ

第一次查询

生成 Read View C

第二次查询

复用 Read View C

READ_COMMITTED

第一次查询

生成 Read View A

第二次查询

生成 Read View B

隔离级别Read View 生成时机效果
READ COMMITTED每条 SELECT 语句执行时生成新 Read View能读到其他事务已提交的新数据 → 不可重复读
REPEATABLE READ第一条 SELECT 语句执行时生成,事务内复用同一事务多次读取结果一致 → 可重复读

5.1 RC 行为示例

MySQL 事务B 事务A MySQL 事务B 事务A 开启事务 开启事务 生成 Read View V1 生成新的 Read View V2 此时事务A已提交 SELECT * FROM user WHERE id=1 返回 age=20 UPDATE user SET age=21 WHERE id=1 COMMIT SELECT * FROM user WHERE id=1 返回 age=21 (❌ 不可重复读)

5.2 RR 行为示例

MySQL 事务B 事务A MySQL 事务B 事务A 开启事务 开启事务 生成 Read View V1 复用 Read View V1 看不到事务A的提交 SELECT * FROM user WHERE id=1 返回 age=20 UPDATE user SET age=21 WHERE id=1 COMMIT SELECT * FROM user WHERE id=1 返回 age=20 (✅ 可重复读)

6. MVCC 实战演示

6.1 准备测试数据

-- 创建测试表
CREATE TABLE `account` (
  `id` int NOT NULL PRIMARY KEY,
  `balance` int DEFAULT 0
) ENGINE=InnoDB;

-- 插入初始数据
INSERT INTO account VALUES (1, 100);

6.2 模拟 MVCC 行为

-- 事务A (开启 RR 隔离级别)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM account WHERE id=1;  -- 结果: 100

-- 事务B (开启独立会话)
BEGIN;
UPDATE account SET balance = 200 WHERE id=1;
COMMIT;

-- 事务A 再次查询
SELECT balance FROM account WHERE id=1;  -- 结果: 100 (复用 Read View)

-- 事务A 提交后再次查询
COMMIT;
SELECT balance FROM account WHERE id=1;  -- 结果: 200 (新事务,新 Read View)

6.3 查看版本链

-- 模拟多次更新制造版本链
BEGIN;
UPDATE account SET balance = 300 WHERE id=1;
COMMIT;

BEGIN;
UPDATE account SET balance = 400 WHERE id=1;
COMMIT;

-- 查看 Undo 信息 (需要特殊工具或开启 monitoring)
-- 在 MySQL 8.0 可以通过 performance_schema 查看
SELECT * FROM performance_schema.data_locks\G

7. MVCC 与锁的协作

很多人误以为 MVCC 完全替代了锁,实则不然——MVCC 仅针对 SELECT 读操作,写操作仍需加锁:

写操作

INSERT

加写锁

UPDATE

DELETE

读操作_SELECT

MVCC
读快照版本
不加锁

当前读
SELECT ... FOR UPDATE/LOCK IN SHARE MODE
加锁

操作类型是否使用 MVCC锁机制
普通 SELECT(快照读)✅ 是不加锁
SELECT … FOR UPDATE❌ 否加行锁/间隙锁
SELECT … LOCK IN SHARE MODE❌ 否加共享锁
INSERT / UPDATE / DELETE❌ 否加排他锁

快照读 vs 当前读

-- 快照读:读取 Read View 中的版本(MVCC)
SELECT * FROM account WHERE id=1;

-- 当前读:读取最新已提交版本(加锁)
SELECT * FROM account WHERE id=1 LOCK IN SHARE MODE;
SELECT * FROM account WHERE id=1 FOR UPDATE;

8. 常见面试题解答

Q1:MVCC 能解决幻读吗?

RR 级别下,MVCC + 间隙锁共同解决幻读:

  • 快照读(普通 SELECT):通过复用 Read View 抑制幻读
  • 当前读(SELECT FOR UPDATE):通过间隙锁锁住范围,阻止插入

Q2:长事务为什么会导致 Undo 膨胀?

因为 Read View 仍持有最早版本,Purge 线程无法清理长事务开始之后生成的 Undo 版本链。

Q3:RR 级别下能读到自己的修改吗?

可以。可见性判断规则中,trx_id = 当前事务ID 时直接可见。

Q4:MySQL 8.0 对 MVCC 有什么优化?

  • Undo Log 可独立表空间,避免系统表空间膨胀
  • 增加 information_schema.INNODB_TRX 等视图方便排查
  • 提升清理效率

9. 总结

解决的问题

MVCC 核心要点

隐藏列
DB_TRX_ID/DB_ROLL_PTR

Undo Log
版本链

Read View
一致性快照

读不加锁
高并发

读写不互斥

可重复读

关键点总结
MVCC 是什么多版本并发控制,让读不加锁、读写不互斥
核心组件隐藏列 + Undo Log 版本链 + Read View
可见性判断基于 Read View 的比较逻辑
RC vs RR区别仅在于 Read View 生成时机
与锁的关系MVCC 仅用于快照读,写操作和当前读仍需加锁
适用隔离级别READ COMMITTED 和 REPEATABLE READ

一句话总结:MVCC 通过保存数据的历史版本,配合 Read View 一致性视图,实现了读写不互斥的并发控制,是 InnoDB 高性能的核心设计之一。


原创不易,欢迎点赞收藏 💡
评论区聊聊:你遇到过 MVCC 相关的什么坑?

标签MySQL MVCC 多版本并发控制 InnoDB 事务隔离 Read View Undo Log

在这里插入图片描述


🌺The End🌺点点关注,收藏不迷路🌺

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_41840843/article/details/161495758

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--