MySQL 的 MVCC 核心原理

83 阅读4分钟

MySQL 的 MVCC(Multi-Version Concurrency Control,多版本并发控制) 是一种通过保留数据历史版本实现高并发的机制,其核心目标是 在保证事务隔离性的前提下,避免读写操作互相阻塞。以下是其核心原理的全解析(基于 InnoDB 引擎):

一、MVCC 的底层核心组件

  1. 隐藏字段

每行数据(聚簇索引)包含 3 个隐藏字段: 字段名 说明

DB_TRX_ID 最近修改该行的事务ID(插入/更新/删除时写入)

DB_ROLL_PTR 回滚指针(指向 undo log 中历史版本数据)

DB_ROW_ID 隐藏主键(当无主键时自动生成)

  1. Undo Log(回滚日志)

• 存储数据修改前的历史版本(链式结构)

• 每个历史版本都包含:DB_TRX_ID + DB_ROLL_PTR + 原始数据

• 示例:
当前行: [值=500, TRX_ID=200, ROLL_PTR→Undo Log A] ↓ Undo Log A: [值=300, TRX_ID=100, ROLL_PTR→Undo Log B] ↓ Undo Log B: [值=200, TRX_ID=50]

  1. Read View(读视图)

事务第一次执行查询时生成,用于决定哪些历史版本对当前事务可见,包含: 字段 说明

m_ids 当前活跃(未提交)的事务ID列表

min_trx_id m_ids 中的最小事务ID

max_trx_id 系统预分配的下一个事务ID(大于所有已存在的事务ID)

creator_trx_id 创建该 Read View 的事务ID

二、MVCC 的可⻅性判断算法

当某事务读取数据行时,按规则遍历版本链直到找到可见版本:

  1. 检查当前行版本号 TRX_ID

  2. 可见条件判断(优先级顺序): • 若 TRX_ID == creator_trx_id → 可见(当前事务自身修改)

    • 若 TRX_ID < min_trx_id → 可见(该版本在事务开启前已提交)

    • 若 TRX_ID >= max_trx_id → 不可见(该版本在事务开启后创建)

    • 若 TRX_ID ∈ [min_trx_id, max_trx_id):

    ◦ TRX_ID ∈ m_ids → 不可见(该版本所属事务未提交)

    ◦ TRX_ID ∉ m_ids → 可见(该版本所属事务已提交)

  3. 若不可见,沿 ROLL_PTR 找到上一个版本重新判断

口诀:

自己改的 ✓ 我开启前提交的 ✓ 我开启后创建的 ✗ 同期间未提交的 ✗

同期间提交的 ✓

三、增删改查如何与 MVCC 交互

  1. SELECT 查询(快照读)

SELECT * FROM account; -- 默认使用快照读

• 生成 Read View

• 沿版本链找到符合 可见性规则 的数据版本

  1. 数据更新(INSERT/UPDATE/DELETE)

• INSERT:写入新行,填充当前事务 ID

• UPDATE:

  1. 将当前行拷贝到 undo log
  2. 更新数据 → 修改值 + 写入新 TRX_ID + 回滚指针指向旧版本
    • DELETE:类似 UPDATE,标记行为 已删除(通过 TRX_ID 标记)

四、MVCC 如何实现不同隔离级别

隔离级别 MVCC 机制实现方式

读提交 (RC) 每次 SELECT 都生成新的 Read View(读到其他事务最新提交的数据)

可重复读 (RR) 只在第一次 SELECT 时生成 Read View(后续读复用该视图 → 实现快照隔离)

未提交读 (RU) 不使用 MVCC,直接读最新数据(含未提交的数据)

⚠️ 幻读的特殊处理

在 RR 级别下,MVCC + Next-Key Lock(间隙锁) 共同解决幻读: • 快照读(SELECT):MVCC 保证读取原始快照 → 避免旧幻读

• 当前读(SELECT FOR UPDATE):使用 Next-Key Lock 锁住可能插入的范围 → 防止新写入

五、生产环境中的 MVCC 关键问题

  1. 长事务的危害

• Undo log 无法及时清理 → 版本链过长导致查询性能下降

• 大事务阻塞 Purge 线程 → 磁盘空间暴涨(ibdata1 膨胀)

解决方案:
• 监控 SELECT * FROM information_schema.innodb_trx;

• 设置 innodb_undo_log_truncate = ON

  1. 读不⻅最新数据的场景

事务 A 快照读后,事务 B 提交更新 → A 的后续读仍无法看到 B 的修改(RR 级别)。
解决方案:
SELECT * FROM account FOR SHARE; -- 加共享锁(走当前读)

  1. 版本清理机制

• 后台 Purge 线程清理无用的 undo log

• 清理条件:版本链中所有记录对当前任何活跃事务都不可⻅

六、MVCC 工作全过程示意图

sequenceDiagram participant T1 as 事务A (TRX_ID=100) participant T2 as 事务B (TRX_ID=200) participant DB as 数据行 + Undo Log

T1->>DB: UPDATE account SET balance=300 (原值200)
DB-->>T1: 创建 undo log1, 回滚指针指向旧值
T1->>DB: 更新行 TRX_ID=100

T2-->>DB: 启动事务,生成 Read View (活跃事务=[100])
T2->>DB: SELECT balance 
DB-->>T2: 检查 TRX_ID=100 (活跃事务ID中 → 不可见)
DB-->>T2: 沿指针读 undo log1 值200 (TRX_ID=50 < min_trx_id → 可见)
T2-->>T2: 返回 balance=200

T1->>T1: COMMIT 提交事务
T2->>DB: SELECT balance (复用原Read View)
DB-->>T2: 仍通过 undo log1 读到200 (保证可重复读)

总结:MVCC 的价值

  1. 读写无阻塞:读操作不阻塞写,写操作不阻塞读
  2. 提升并发能力:RC/RR 级别下大幅减少锁争用
  3. 快速回滚:基于 undo log 轻松实现事务回滚
  4. 隔离性保障:灵活支持不同级别的事务隔离需求

⚠️ 注意:MVCC 仅优化读操作,并发写依然需要锁机制(如行锁、间隙锁)。