6.6 KiB
6.6 KiB
tags, create time, update time
| tags | create time | update time | |||||
|---|---|---|---|---|---|---|---|
|
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 + 1m_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
简化记忆口诀:
- 小于 up_limit_id → 肯定可见(生成 ReadView 前就提交了)
- 大于等于 low_limit_id → 肯定不可见(生成 ReadView 后才启动的)
- 介于两者之间 → 再看是否在活跃列表中,以及是不是自己
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 异常休眠的线程