Files
autumn-recruitment/02.MySQL/transaction/ACID 与 MVCC 机制.md
T

6.6 KiB
Raw Blame History

tags, create time, update time
tags create time update time
mysql
acido-mvcc
undo-log
read-view
repeatable-read
2026-08-08 18:00 2026-08-08 18:00

ACID 与 MVCC 机制

概述

MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB 实现幻读隔离的核心机制。它通过维护数据的多个历史版本,使得读写操作互不阻塞,在保证事务隔离性的同时大幅提升并发性能。理解 MVCC 的实现细节——undo log、Read View、可见性判断——对于深入把握 MySQL 事务行为至关重要。

核心原理

Undo Log 的结构

Undo Log 是 InnoDB 为了实现 MVCC 和事务回滚而维护的一种日志,它记录了事务修改前的旧数据版本。

graph TD
    subgraph "Buffer Pool — 当前最新版本"
        RowA["行记录 A<br/>version=3, TRX_ID=T5"]
    end

    subgraph "Undo Log Chain — 历史版本链"
        Version3["版本3: val='updated_v3'<br/>TRX_ID=T5, next_undo=Version2"]
        Version2["版本2: val='updated_v2'<br/>TRX_ID=T4, next_undo=Version1"]
        Version1["版本1: val='original_val'<br/>TRX_ID=T3, next_undo=NULL"]
    end

    RowA -->|"DB_TRX_ID 指向"| Version3
    Version3 --> Version2
    Version2 --> Version1

Undo Log 的两类内容:

  • redo log:物理日志,记录"在某处做了什么修改",用于崩溃恢复
  • undo log:逻辑日志,记录"做了什么事情",用于回滚和 MVCC

每个行记录隐藏了两列:

  • DB_TRX_ID:最近修改该行的事务 ID(6 字节)
  • DB_ROLL_PTR:回滚指针,指向 undo log 中上一个版本的地址(7 字节)

[!NOTE] Rollback Segment 的组织方式 InnoDB 将 undo log 组织在 Rollback Segment 中,分为 undo log header 和 undo log record 两部分。每个事务的修改按时间顺序追加到 segment 尾部,形成版本号链。

Read View 的生成规则

Read View 是 MVCC 的核心数据结构,定义了事务在某一时刻"能看到哪些版本"。

graph TD
    subgraph "RC 隔离级别 - ReadView 生成时机"
        RC_T1["事务开始"] --> RC_Q1["第一次查询时"]
        RC_Q1 --> RC_ReadView["生成 ReadView"]
        RC_ReadView --> RC_Q2["第二次查询时"]
        RC_Q2 --> RC_NewRV["再次生成新的 ReadView"]
    end

    subgraph "RR 隔离级别 - ReadView 生成时机"
        RR_T1["事务开始"] --> RR_FirstQ["第一次查询时"]
        RR_FirstQ --> RR_ReadView["生成 ReadView"]
        RR_ReadView --> RR_Q2["后续查询"]
        RR_Q2 --> RR_SameRV["复用同一个 ReadView"]
    end

    style RC_ReadView fill:#ffeeaa
    style RC_NewRV fill:#ffeeaa
    style RR_ReadView fill:#99ffcc
    style RR_SameRV fill:#99ffcc

RC(Read Committed)和 RR(Repeatable Read)的关键区别:

特性 RC RR
ReadView 生成时机 每条 SELECT 语句开始时生成 第一次查询时生成,后续复用
快照读可见性 每次查询都是最新提交的快照 全局一致视图,所有查询所见相同
解决幻读能力 不能解决 配合 Next-Key Lock 可以解决

ReadView 的成员变量:

  • m_ids:生成 ReadView 时当前活跃的事务 ID 列表
  • m_low_limit_id:最小的活跃事务 ID(下一个将要分配的事务 ID)
  • m_up_limit_id:最大活跃事务 ID + 1
  • m_trx_id_level:活跃事务的最小 trx_id

可见性判断流程

这是 MVCC 最核心的算法:给定一个行版本和一个 ReadView,判断当前事务是否能看到这个版本。

flowchart TD
    Start["开始: 行版本 trx_id + ReadView"] --> Compare1{"trx_id < m_up_limit_id?"}
    
    Compare1 -->|否| CheckActive{"trx_id ∈ m_ids?"}
    Compare1 -->|是| Visible["✓ 可见"]
    
    CheckActive -->|否| Visible
    CheckActive -->|是| Compare2{"trx_id < m_low_limit_id?"}
    
    Compare2 -->|是| Invisible["✗ 不可见<br/>事务尚未提交"]
    Compare2 -->|否| CheckOwn{"trx_id = 当前事务ID?"}
    
    CheckOwn -->|是| VisibleSelf["✓ 可见<br/>自己修改的版本"]
    CheckOwn -->|否| Compare3{"trx_id 已提交?"}
    
    Compare3 -->|是| Visible
    Compare3 -->|否| Invisible
    
    style Visible fill:#90ee90
    style VisibleSelf fill:#90ee90
    style Invisible fill:#ff9999

简化记忆口诀:

  1. 小于 up_limit_id → 肯定可见(生成 ReadView 前就提交了)
  2. 大于等于 low_limit_id → 肯定不可见(生成 ReadView 后才启动的)
  3. 介于两者之间 → 再看是否在活跃列表中,以及是不是自己

Next-Key 与 ReadView 的配合

RR 隔离级别下,InnoDB 用"MVCC + Next-Key Lock"双管齐下来解决幻读:

  • 快照读(普通 SELECT)靠 MVCC/ReadView 解决
  • 当前读(SELECT ... FOR UPDATE / LOCK IN SHARE MODE / UPDATE / DELETE)靠 Next-Key Lock 解决
-- 快照读:不会锁住区间,依赖 ReadView 保证一致性
SELECT * FROM orders WHERE status = 1;

-- 当前读:加锁,防止其他事务插入或修改
SELECT * FROM orders WHERE status = 1 FOR UPDATE;

代码示例

-- 演示 RC vs RR 在读已提交数据时的差异

-- 会话 1:设置隔离级别为 RR(默认)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
-- 此时 balance = 1000

-- 会话 2:修改并提交
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
UPDATE accounts SET balance = 2000 WHERE id = 1;
COMMIT;

-- 会话 1:再次查询
SELECT balance FROM accounts WHERE id = 1;
-- RR: 仍然返回 1000(复用了初始 ReadView)
-- 如果换成 RC: 返回 2000(重新生成 ReadView)

实践场景

场景一:理解为什么 RR 下"看不到别人刚提交的数据"

  • 在 RR 中,事务内的所有快照读看到的是同一个一致性视图
  • 如果你需要在事务中看到其他事务的最新提交结果,可以在适当时候设置 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;(需单独事务生效)或在应用层做补偿查询

场景二:监控 undo log 空间

-- 查看 undo log 使用情况
SELECT * FROM sys.innodb_old_tablespaces;

-- 长时间运行的未提交事务会产生大量 undo 记录
-- 影响 buffer pool 的内存占用
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id
FROM information_schema.innodb_trx
WHERE trx_state = 'RUNNING' AND TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;

场景三:避免长事务导致的 undo 膨胀

  • 不要在事务中进行大量无关操作
  • 批量更新拆分成小批次事务
  • 定时清理长时间运行的事务:kill 异常休眠的线程

扩展阅读