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

182 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<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 的核心数据结构,定义了事务在某一时刻"能看到哪些版本"。
```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["✗ 不可见<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 解决
```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+树索引原理]]
- [[事务隔离级别与锁机制]]