--- tags: [mysql, acido-mvcc, undo-log, read-view, repeatable-read] create time: 2026-08-08 18:00 update time: 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 和事务回滚而维护的一种日志,它记录了事务修改前的旧数据版本。 ```mermaid graph TD subgraph "Buffer Pool — 当前最新版本" RowA["行记录 A
version=3, TRX_ID=T5"] end subgraph "Undo Log Chain — 历史版本链" Version3["版本3: val='updated_v3'
TRX_ID=T5, next_undo=Version2"] Version2["版本2: val='updated_v2'
TRX_ID=T4, next_undo=Version1"] Version1["版本1: val='original_val'
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 的核心数据结构,定义了事务在某一时刻"能看到哪些版本"。 ```mermaid 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,判断当前事务是否能看到这个版本。 ```mermaid 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["✗ 不可见
事务尚未提交"] Compare2 -->|否| CheckOwn{"trx_id = 当前事务ID?"} CheckOwn -->|是| VisibleSelf["✓ 可见
自己修改的版本"] 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 解决 ```sql -- 快照读:不会锁住区间,依赖 ReadView 保证一致性 SELECT * FROM orders WHERE status = 1; -- 当前读:加锁,防止其他事务插入或修改 SELECT * FROM orders WHERE status = 1 FOR UPDATE; ``` ## 代码示例 ```sql -- 演示 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 空间** ```sql -- 查看 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 异常休眠的线程 ## 扩展阅读 - [[B+树索引原理]] - [[事务隔离级别与锁机制]]