乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 数据库基础与安装

    • MySQL 简介
    • MySQL 5.6 到 5.7 到 8.0 关键差异速查
    • 安装 MySQL 5.7
    • 连接与断开服务器
    • 创建数据库
    • 创建数据表
    • 数据库与数据表
    • 加载数据
    • 获取数据库信息
    • 批处理模式
    • SHOW 语句汇总
    • FLUSH 与 RESET 语句
    • my.cnf 核心参数
    • 字符集与排序规则
  • 第2章 SQL基础查询

    • SELECT
    • WHERE
    • ORDER BY
    • LIMIT
    • COUNT
    • 聚合函数
    • 比较运算符
    • 逻辑运算符
    • 算术运算符
    • 模式匹配
    • NULL 值处理
    • UPDATE
    • DELETE
    • REPLACE
    • SELECT INTO
  • 第3章 数据类型与运算符

    • 数值类型
    • 字符串类型
    • 日期时间类型
    • BIT 类型
    • ENUM 类型
    • SET 类型
    • JSON 类型
    • 类型转换
  • 第4章 函数与表达式

    • 字符串函数
    • 数值函数
    • 日期函数
    • 全文检索函数
  • 第5章 高级查询与子查询

    • JOIN
    • 子查询
    • UNION
    • GROUP BY
    • HAVING
    • DISTINCT
  • 第6章 表与索引

    • 数据定义语言
    • 修改表结构
    • 视图
    • 修改视图与检查选项
    • 外键
    • 索引
    • 唯一索引
    • 复合索引
    • 存储引擎对比
    • 分区表
    • 第一范式与第二范式
    • 第三范式与 BC 范式
    • 反范式设计
  • 第7章 存储过程与函数

    • 存储过程
    • 存储函数
    • 变量
    • 流程控制
    • 游标
    • 预处理语句
  • 第8章 事务与锁

    • 事务
    • 事务隔离级别
    • 锁机制
    • MVCC
    • 死锁专题分析
    • LOCK TABLES
    • XA 事务
  • 第9章 用户管理与安全

    • 用户管理
    • 权限管理
    • 角色
    • SQL 注入防范
  • 第10章 性能优化入门

    • 执行计划
    • 索引优化
    • 查询优化
    • 查询优化器提示
    • 慢查询日志
    • InnoDB 深入机制
    • InnoDB 专项优化
    • Performance Schema
    • sys Schema
  • 第11章 复制与高可用

    • 主从复制原理
    • 半同步复制配置
    • binlog 开启与 point-in-time 恢复
    • mysqldump 全库备份
    • mysqldump 单表与条件备份
    • mysqldump 恢复与导入
    • xtrabackup 全量热备
    • xtrabackup 准备与恢复
    • xtrabackup 增量与流式备份
  • 第12章 触发器与事件

    • 触发器
    • 事件调度器
  • 参考

    • MySQL 5.7 专业术语大全
    • MySQL 5.7 关键字与保留字大全

MVCC

导学

MySQL 的 REPEATABLE READ 隔离级别下,普通 SELECT 几乎不加锁,却能保证可重复读——这是怎么做到的?答案是MVCC(多版本并发控制)。它是 InnoDB 实现高并发的核心技术之一。

定义

MVCC(Multi-Version Concurrency Control):通过保存数据在某一时刻的快照(Snapshot),让读操作不需要加锁就能获取一致的数据视图,从而实现读写互不阻塞的并发控制机制。

MVCC 的实现:隐藏列和 Undo Log

InnoDB 的每行记录都有两个隐藏列:

隐藏列含义
DB_TRX_ID最后修改该行的事务 ID(6 字节)
DB_ROLL_PTR回滚指针,指向 Undo Log 中的上一个版本(7 字节)

当某行被修改时,InnoDB 不会直接覆盖原数据,而是:

  1. 将原数据复制到 Undo Log
  2. 修改当前行的数据
  3. 将当前行的 DB_ROLL_PTR 指向 Undo Log 中的旧版本

这就形成了一条版本链。

ReadView:决定你能看到哪个版本

快照读 vs 当前读

读取类型SQL 形式是否加锁读取数据来源
快照读(Snapshot Read)普通 SELECT不加锁历史版本(Undo Log)
当前读(Current Read)SELECT ... FOR UPDATE / IN SHARE MODE加锁最新版本

ReadView 的生成时机

-- READ COMMITTED:每次查询生成新的 ReadView
-- REPEATABLE READ:事务开始时生成 ReadView,整个事务复用

ReadView 包含以下信息:

  • creator_trx_id:创建该 ReadView 的事务 ID
  • m_ids:生成 ReadView 时,所有**活跃(未提交)**事务的 ID 列表
  • min_trx_id:m_ids 中的最小值
  • max_trx_id:生成 ReadView 时,下一个将被分配的事务 ID

可见性判断规则

对于某行的 DB_TRX_ID:

  1. DB_TRX_ID == creator_trx_id:自己修改的,可见
  2. DB_TRX_ID < min_trx_id:在 ReadView 生成前已提交,可见
  3. DB_TRX_ID >= max_trx_id:在 ReadView 生成后启动,不可见
  4. min_trx_id <= DB_TRX_ID < max_trx_id:
    • 如果 DB_TRX_ID 在 m_ids 中(活跃事务):不可见,沿 Undo Log 找上一个版本
    • 如果不在 m_ids 中(已提交):可见

SQL 示例:MVCC 实现可重复读

准备测试数据

CREATE TABLE employees (
    emp_id INT PRIMARY KEY AUTO_INCREMENT,
    emp_name VARCHAR(20),
    dept VARCHAR(20),
    score DECIMAL(5,2)
);

INSERT INTO employees (emp_name, dept, score) VALUES
('大翔', '技术部', 100),
('白歌', '技术部', NULL);

当前数据状态:

emp_idemp_namedeptscore
1大翔技术部100
2白歌技术部NULL

假设初始状态的隐藏列:

  • DB_TRX_ID = 50(某个已提交的事务修改了这行)
  • DB_ROLL_PTR 指向 Undo Log 中的历史版本

场景一:MVCC 实现可重复读

操作过程:两个会话并发执行

时间顺序会话 A(事务 ID = 100)会话 B(事务 ID = 101)
T1START TRANSACTION;
T2SELECT score FROM employees WHERE emp_id = 1; -- 结果:100
T3START TRANSACTION;
T4UPDATE employees SET score = 80 WHERE emp_id = 1;
T5COMMIT;
T6SELECT score FROM employees WHERE emp_id = 1; -- 结果:100(可重复读!)
T7COMMIT;
T8SELECT score FROM employees WHERE emp_id = 1; -- 结果:80

完整 SQL 演示:

-- ========== 会话 A(事务 ID = 100) ==========
START TRANSACTION;

-- 第一次查询:ReadView 生成
-- m_ids = {100}, min_trx_id = 100, max_trx_id = 101, creator_trx_id = 100
SELECT score FROM employees WHERE emp_id = 1;

当前数据状态(T2):

score
100

结果解读:

  • 会话 A 的 ReadView 在第一次查询时生成
  • 当前行的 DB_TRX_ID = 50(假设初始值)
  • 50 < min_trx_id = 100,说明在 ReadView 生成前已提交,可见
  • 所以读到 score = 100
-- ========== 会话 B(事务 ID = 101) ==========
START TRANSACTION;

-- 修改数据
UPDATE employees SET score = 80 WHERE emp_id = 1;
-- 此时 InnoDB 的操作:
-- 1. 将原数据(score=100, DB_TRX_ID=50)复制到 Undo Log
-- 2. 修改当前行 score = 80
-- 3. 当前行的 DB_TRX_ID 变为 101
-- 4. DB_ROLL_PTR 指向 Undo Log 中的旧版本

COMMIT;

会话 B 提交后的实际数据状态(最新版本):

emp_idscoreDB_TRX_IDDB_ROLL_PTR
1大翔技术部80
-- ========== 会话 A 继续 ==========
-- 第二次查询:仍然使用事务开始时生成的 ReadView
-- m_ids = {100}, min_trx_id = 100, max_trx_id = 101
SELECT score FROM employees WHERE emp_id = 1;

当前数据状态(T6,会话 A 的视角):

score
100

结果解读:

  • 当前行的 DB_TRX_ID = 101(会话 B 的修改)
  • 101 >= max_trx_id = 101,说明在 ReadView 生成后启动,不可见
  • 沿 Undo Log 找到上一个版本:score = 100, DB_TRX_ID = 50
  • 50 < min_trx_id = 100,可见
  • 所以仍然读到 score = 100,实现了可重复读!
-- 会话 A 提交
COMMIT;

-- 新事务(事务 ID = 102),生成新的 ReadView
START TRANSACTION;
SELECT score FROM employees WHERE emp_id = 1;

操作后的数据状态(新事务的视角):

score
80

结果解读:

  • 新事务的 ReadView:m_ids = {102}, min_trx_id = 102, max_trx_id = 103
  • 当前行的 DB_TRX_ID = 101
  • 101 < min_trx_id = 102,说明在 ReadView 生成前已提交,可见
  • 所以读到 score = 80(最新已提交数据)
COMMIT;

场景二:READ COMMITTED 下的不可重复读

操作过程

-- ========== 会话 A ==========
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;

-- 第一次查询:生成 ReadView
SELECT score FROM employees WHERE emp_id = 1;

当前数据状态:

score
80
-- ========== 会话 B ==========
START TRANSACTION;
UPDATE employees SET score = 60 WHERE emp_id = 1;
COMMIT;

-- ========== 会话 A 继续 ==========
-- 第二次查询:在 RC 下,每次查询生成新的 ReadView
SELECT score FROM employees WHERE emp_id = 1;

当前数据状态(RC 级别下):

score
60

结果解读:

  • RC 级别下,每次查询都生成新的 ReadView
  • 第二次查询时,新的 ReadView 看到 DB_TRX_ID = 101(或新的事务 ID)已经提交
  • 所以读到了最新的 score = 60
  • 同一事务内两次查询结果不同,这就是不可重复读
COMMIT;

场景三:自己修改的数据立即可见

-- ========== 会话 A ==========
START TRANSACTION;

-- 查询原始数据
SELECT score FROM employees WHERE emp_id = 1;

当前数据状态:

score
60
-- 自己修改数据
UPDATE employees SET score = 50 WHERE emp_id = 1;

-- 再次查询
SELECT score FROM employees WHERE emp_id = 1;

当前数据状态(同一事务内):

score
50

结果解读:

  • 当前行的 DB_TRX_ID 变为当前事务的 ID
  • 根据可见性规则 1:DB_TRX_ID == creator_trx_id,自己修改的立即可见
  • 所以同一事务内能立即看到自己修改的数据
COMMIT;

MVCC 与 Purge

Undo Log 中的旧版本不能无限增长。InnoDB 的Purge 线程会定期清理不再被任何 ReadView 需要的旧版本。

⚠️ 关键风险:长事务(长时间不提交)会阻止 Purge 清理,导致 Undo Log 膨胀,严重影响性能。生产环境应监控长事务并设置超时。

示例:长事务导致 Undo Log 膨胀

-- 会话 A 开启一个长时间不提交的事务
START TRANSACTION;

-- 执行查询,生成 ReadView
SELECT score FROM employees WHERE emp_id = 1;

当前数据状态:

score
50
-- ========== 大量其他事务在此期间修改数据 ==========
-- 会话 B、C、D ... 不断修改 employees 表的数据
-- 每次修改都会产生新的 Undo Log 记录

-- 但由于会话 A 的事务未提交,其 ReadView 仍然有效
-- Purge 线程无法清理 Undo Log 中比 ReadView 更早的版本

-- 假设 10 分钟后,会话 A 才提交
COMMIT;

-- 现在 Purge 线程才能清理旧的 Undo Log

结果解读:

  • 会话 A 的 ReadView 持有时间长达 10 分钟
  • 在这期间,所有被修改的数据的旧版本都必须保留在 Undo Log 中
  • Undo Log 文件会不断膨胀,占用大量磁盘空间
  • 其他查询如果需要遍历版本链,性能也会下降
  • 生产环境建议:设置 innodb_lock_wait_timeout 和监控长事务
-- 查看当前运行中的长事务
SELECT 
    trx_id,
    trx_state,
    trx_started,
    TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_seconds,
    trx_mysql_thread_id,
    trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;

示例输出:

trx_idtrx_statetrx_startedtrx_secondstrx_mysql_thread_idtrx_query
12345RUNNING2024-01-15 10:00:00600100SELECT score FROM employees WHERE emp_id = 1

结果解读:

  • trx_seconds = 600:该事务已经运行了 600 秒(10 分钟)
  • 应设置告警,当事务运行时间超过阈值(如 60 秒)时通知 DBA

常见误区

误区正解
MVCC 完全不加锁快照读不加锁,但当前读(FOR UPDATE)仍然加锁。
MVCC 没有空间开销Undo Log 需要磁盘空间,长事务会导致 Undo Log 膨胀。
RR 级别完全无幻读MVCC 解决了快照读的幻读,但当前读(FOR UPDATE)的幻读需要 Gap Lock 解决。

示例:快照读与当前读的区别

-- 准备数据(使用 employees 表)
SELECT * FROM employees WHERE emp_id = 1;

当前数据状态:

emp_idemp_namedeptscore
1大翔技术部100
-- ========== 会话 A ==========
START TRANSACTION;

-- 快照读(普通 SELECT):读取历史版本,不加锁
SELECT * FROM employees WHERE emp_id = 1;

快照读结果:

emp_idemp_namedeptscore
1大翔技术部100
-- 当前读(FOR UPDATE):读取最新版本,加 X 锁
SELECT * FROM employees WHERE emp_id = 1 FOR UPDATE;

当前读结果:

emp_idemp_namedeptscore
1大翔技术部100
COMMIT;

结果解读:

  • 快照读和当前读在当前场景下结果相同
  • 但如果有其他事务在同时修改数据:
    • 快照读读到的是历史版本(不受其他事务影响)
    • 当前读读到的是最新版本,且会阻塞其他事务的修改

面试考点

Q:什么是 MVCC?

多版本并发控制。通过保存数据的多个历史版本(Undo Log),让读操作读取历史快照而不加锁,实现读写并发互不阻塞。每行记录有 DB_TRX_ID 和 DB_ROLL_PTR 两个隐藏列。

Q:READ COMMITTED 和 REPEATABLE READ 在 MVCC 下的区别?

RC 每次查询生成新的 ReadView,能看到其他事务已提交的修改;RR 在事务开始时生成 ReadView,事务内始终看到同一快照,实现可重复读。

Q:为什么长事务会导致性能下降?

长事务持有旧的 ReadView,阻止 Purge 线程清理 Undo Log,导致 Undo Log 不断膨胀,占用大量磁盘空间,同时增加查询时遍历版本链的开销。

Q:MVCC 能解决幻读吗?

MVCC 解决了快照读(普通 SELECT)的幻读。当前读(SELECT ... FOR UPDATE)的幻读需要通过 Gap Lock / Next-Key Lock 解决。

小结

  • MVCC 通过保存数据的多版本实现读写并发不阻塞
  • 每行有 DB_TRX_ID(事务 ID)和 DB_ROLL_PTR(回滚指针)
  • ReadView 决定事务能看到哪个版本的数据
  • RC 每次查询新 ReadView,RR 事务开始时生成 ReadView
  • 长事务会阻止 Purge,导致 Undo Log 膨胀,应尽量避免

本章结束:到这里,你已经系统掌握了 MySQL 5.7 的核心知识——从基础查询到高级优化,从表结构设计到事务并发控制。数据库是一门实践科学,建议你在真实环境中多加练习,用 EXPLAIN 分析每一条慢 SQL,用慢查询日志发现瓶颈,用事务和锁保证数据一致性。

上一页
锁机制
下一页
死锁专题分析