182 lines
6.6 KiB
Markdown
182 lines
6.6 KiB
Markdown
|
|
---
|
|||
|
|
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+树索引原理]]
|
|||
|
|
- [[事务隔离级别与锁机制]]
|