MySQL MVCC(多版本并发控制)的工作流程及原理。
一、MVCC 是什么?
MVCC(Multi-Version Concurrency Control) 是一种并发控制机制,核心思想是:
读写不阻塞:写操作生成新版本,读操作读取旧版本
scss
传统锁机制(读写互斥) MVCC(读写并行)
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 读操作 │◄──►│ 写操作 │ │ 读操作 │ │ 写操作 │
│ (阻塞) │ │ (阻塞) │ │ (读旧版) │ │ (写新版) │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
互相等待 互不干扰
二、核心概念:三个隐藏字段
InnoDB 每行记录都有 3 个隐藏字段:
| 字段名 | 长度 | 作用 |
|---|---|---|
DB_TRX_ID |
6 字节 | 最后修改该行的事务 ID |
DB_ROLL_PTR |
7 字节 | 回滚指针,指向 undo log |
DB_ROW_ID |
6 字节 | 隐藏主键(若无显式主键) |
bash
┌─────────────────────────────────────────────────────────┐
│ 行记录结构示例 │
├─────────────────────────────────────────────────────────┤
│ id │ name │ age │ DB_TRX_ID │ DB_ROLL_PTR │
│ 1 │ 张三 │ 20 │ 100 │ 0x7f8b... │
└─────────────────────────────────────────────────────────┘
↑ 用户可见字段 ↑ 隐藏字段(对用户透明)
三、核心概念:Undo Log(回滚日志)
Undo Log 是 MVCC 的版本链基础,记录数据修改前的状态:
arduino
版本链(同一行数据的多个版本):
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 版本3 │◄────│ 版本2 │◄────│ 版本1 │◄────│ 初始版本 │
│ TRX_ID= │ │ TRX_ID= │ │ TRX_ID= │ │ TRX_ID= │
│ 300 │ │ 200 │ │ 100 │ │ 50 │
│ name= │ │ name= │ │ name= │ │ name= │
│ "王五" │ │ "李四" │ │ "张三" │ │ "初始" │
└────┬────┘ └─────────┘ └─────────┘ └─────────┘
│
└── DB_ROLL_PTR 指向这里(最新版本指向旧版本)
四、核心概念:Read View(读视图)
Read View 是快照读时的"可见性判断器",决定当前事务能看到哪个版本的数据。
Read View 的四个关键属性:
sql
┌─────────────────────────────────────────────────────────┐
│ Read View 结构 │
├─────────────────────────────────────────────────────────┤
│ creator_trx_id │ 创建该 Read View 的事务 ID │
│ m_ids │ 活跃事务 ID 列表(未提交的事务) │
│ min_trx_id │ 最小活跃事务 ID │
│ max_trx_id │ 下一个分配的事务 ID(已创建的最大+1) │
└─────────────────────────────────────────────────────────┘
可见性判断规则(核心算法):
python
def is_visible(trx_id, read_view):
"""
判断某版本的 trx_id 对当前事务是否可见
"""
if trx_id == read_view.creator_trx_id:
# 规则1:自己修改的数据,总是可见
return True
if trx_id < read_view.min_trx_id:
# 规则2:在 Read View 创建前已提交,可见
return True
if trx_id >= read_view.max_trx_id:
# 规则3:在 Read View 创建后才启动,不可见
return False
if trx_id in read_view.m_ids:
# 规则4:在活跃列表中(未提交),不可见
return False
# 规则5:不在活跃列表中(已提交),可见
return True
五、MVCC 工作流程图解
场景:事务 A 读取,事务 B 修改同一行
ini
时间线 ───────────────────────────────────────────────►
T1: 事务 A 启动(TRX_ID=100)
└─► 创建 Read View: {creator=100, m_ids=[100], min=100, max=101}
T2: 事务 B 启动(TRX_ID=101)
└─► 开始修改 id=1 的行
T3: 事务 B 修改 id=1 的 name="李四"(未提交)
└─► 生成新版本:TRX_ID=101, 旧版本进入 Undo Log
┌─────────┐
│ 新版本 │ TRX_ID=101, name="李四"
└────┬────┘
│ DB_ROLL_PTR
▼
┌─────────┐
│ 旧版本 │ TRX_ID=50, name="张三" ◄── 事务 A 能看到
└─────────┘
T4: 事务 A 查询 id=1
└─► 检查最新版本 TRX_ID=101
├─► 101 >= max_trx_id(101)? 是,但 101 在 m_ids 中
├─► 101 在 m_ids 中?是,不可见!
└─► 沿回滚指针找到旧版本 TRX_ID=50
├─► 50 < min_trx_id(100)?是,可见!
└─► 返回 name="张三" ✓
T5: 事务 B 提交
T6: 事务 A 再次查询 id=1(同一事务内)
└─► 仍使用 T1 创建的 Read View
└─► 结果仍是 name="张三"(可重复读!)
六、RC vs RR 的区别:Read View 创建时机
| 隔离级别 | Read View 创建时机 | 效果 |
|---|---|---|
| RC(读已提交) | 每条 SELECT 都创建新的 | 能看到其他事务已提交的最新数据 |
| RR(可重复读) | 事务启动时创建,全程复用 | 事务内多次读取结果一致 |
sql
RC 工作流程: RR 工作流程:
SELECT #1 ──► 创建 RV1 START ──────► 创建 RV
SELECT #2 ──► 创建 RV2 (看到新提交) SELECT #1 ──► 使用 RV
SELECT #3 ──► 创建 RV3 (看到新提交) SELECT #2 ──► 使用 RV(同结果)
SELECT #3 ──► 使用 RV(同结果)
七、完整示例演示
表结构:
sql
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(20),
age INT
) ENGINE=InnoDB;
INSERT INTO user VALUES (1, '张三', 20); -- 假设 TRX_ID=1 插入
并发事务执行:
| 时间 | 事务 A (TRX_ID=100) | 事务 B (TRX_ID=101) | 数据版本链 |
|---|---|---|---|
| t1 | START TRANSACTION; |
V1: TRX_ID=1, name="张三" | |
| t2 | START TRANSACTION; |
||
| t3 | UPDATE user SET name='李四' WHERE id=1; |
V2: TRX_ID=101 → V1 | |
| t4 | SELECT * FROM user WHERE id=1; |
返回 "张三" | |
| t5 | COMMIT; |
V2 已提交 | |
| t6 | SELECT * FROM user WHERE id=1; |
RR返回"张三",RC返回"李四" | |
| t7 | COMMIT; |
t4 时刻的可见性判断(RR):
ini
Read View: {creator=100, m_ids=[100,101], min=100, max=102}
检查 V2 (TRX_ID=101):
- 101 != 100(不是自己)
- 101 >= 102? 否
- 101 在 m_ids 中? 是 → 不可见!
沿回滚指针找到 V1 (TRX_ID=1):
- 1 < 100? 是 → 可见!
结果: name="张三"
八、MVCC 解决并发问题
| 问题 | 解决方案 |
|---|---|
| 脏读(Dirty Read) | 读取时判断,未提交事务的版本不可见 |
| 不可重复读(Non-repeatable Read) | RR 级别复用 Read View |
| 幻读(Phantom Read) | 部分解决(快照读),当前读需加间隙锁 |
注意:MVCC 不解决幻读的场景
sql
-- 事务 A
START TRANSACTION;
SELECT * FROM user WHERE age > 18; -- 快照读,返回 2 条(MVCC 保证)
-- 事务 B 插入新记录并提交
SELECT * FROM user WHERE age > 18; -- 仍是 2 条(MVCC 保证)
SELECT * FROM user WHERE age > 18 FOR UPDATE; -- 当前读!返回 3 条(幻读!)
九、总结图
css
┌─────────────────────────────────────────────────────────┐
│ MVCC 核心架构 │
├─────────────────────────────────────────────────────────┤
│ 存储层: 行记录 + 隐藏字段(DB_TRX_ID, DB_ROLL_PTR) │
│ │ │
│ ▼ │
│ 版本链: Undo Log 串联的历史版本 │
│ │ │
│ ▼ │
│ 可见性: Read View 判断哪个版本对当前事务可见 │
│ │ │
│ ▼ │
│ 隔离性: RC(每次读新建) / RR(事务开始时建) │
└─────────────────────────────────────────────────────────┘
总结 :MVCC 通过保存数据的历史版本 + 事务可见性判断 ,实现了读不阻塞写,写不阻塞读的高效并发控制。