MVCC (Multi-Version Concurrency Control) 多版本并发控制
是什么
MVCC 是一种数据库并发控制机制,让读操作不加锁,通过保存数据的多个版本,实现读写互不阻塞。
核心目标:读不阻塞写,写不阻塞读,解决读写冲突问题。
怎么用
MVCC 是数据库引擎内部机制,不需要开发者手动使用。在 MySQL InnoDB 中:
- 普通 SELECT 自动走 MVCC 快照读(不加锁)
- 加锁读 (
SELECT ... FOR UPDATE / LOCK IN SHARE MODE)走当前读(不走 MVCC)
sql
-- 快照读,走MVCC,不加锁
SELECT * FROM account WHERE id = 1;
-- 当前读,不走MVCC,加锁
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- 写操作也是当前读
UPDATE account SET balance = balance - 100 WHERE id = 1;
实现原理(InnoDB)
InnoDB 的 MVCC 依赖三个核心组件:
1. 隐藏字段
每行数据有两个隐藏列:
| 隐藏字段 | 说明 |
|---|---|
DB_TRX_ID |
最后修改该行的事务 ID |
DB_ROLL_PTR |
回滚指针,指向 undo log 中的上一个版本 |
2. Undo Log(版本链)
每次修改数据时,旧版本写入 undo log,通过 DB_ROLL_PTR 串成版本链:
ini
当前行 (trx_id=5, roll_ptr→)
→ 旧版本v2 (trx_id=3, roll_ptr→)
→ 旧版本v1 (trx_id=1, roll_ptr=NULL)
3. Read View(读视图)
事务执行快照读时生成,决定当前事务能看到哪个版本。包含四个关键字段:
| 字段 | 说明 |
|---|---|
m_ids |
生成 Read View 时,所有活跃(未提交)事务 ID 列表 |
min_trx_id |
活跃事务中最小 ID |
max_trx_id |
下一个将分配的事务 ID(即当前最大事务 ID + 1) |
creator_trx_id |
创建该 Read View 的事务 ID |
可见性判断规则
对版本链上某个版本,按以下顺序判断:
markdown
1. trx_id == creator_trx_id → 可见(自己修改的)
2. trx_id < min_trx_id → 可见(事务已提交)
3. trx_id >= max_trx_id → 不可见(事务在 Read View 之后才开始)
4. min_trx_id <= trx_id < max_trx_id:
- trx_id 在 m_ids 中 → 不可见(事务未提交)
- trx_id 不在 m_ids 中 → 可见(事务已提交)
5. 当前版本不可见 → 沿 roll_ptr 找上一个版本,重复判断
具体例子
场景:账户转账
sql
CREATE TABLE account (
id INT PRIMARY KEY,
name VARCHAR(50),
balance INT
) ENGINE=InnoDB;
INSERT INTO account VALUES (1, 'Alice', 1000); -- trx_id=1, 已提交
此时数据行:id=1, name='Alice', balance=1000, DB_TRX_ID=1, DB_ROLL_PTR=NULL
并发操作
ini
时间线:
T1: 事务A (trx_id=2) 开始,未提交
T2: 事务B (trx_id=3) 开始,未提交
T3: 事务A UPDATE account SET balance=800 WHERE id=1;
→ 旧版本写入undo log,当前行 DB_TRX_ID=2, DB_ROLL_PTR→旧版本(balance=1000, trx_id=1)
T4: 事务B SELECT * FROM account WHERE id=1; -- 快照读
T5: 事务C (trx_id=4) UPDATE account SET balance=600 WHERE id=1; -- 等待事务A提交
T4 时刻事务B的 Read View:
m_ids = [2, 3](事务A、B都未提交)min_trx_id = 2max_trx_id = 5creator_trx_id = 3
判断可见性:
- 当前行
trx_id=2,2 < min_trx_id(2)?否 2 >= max_trx_id(5)?否2在m_ids=[2,3]中?是 → 不可见(事务A未提交)- 沿
roll_ptr找到旧版本trx_id=1 1 < min_trx_id(2)?是 → 可见
结果:事务B 读到 balance=1000(事务A修改前的值)
RC vs RR 的区别
| 隔离级别 | Read View 生成时机 | 效果 |
|---|---|---|
| RC(读已提交) | 每次 SELECT 都生成新 Read View | 能读到其他事务已提交的最新值 |
| RR(可重复读) | 事务内第一次 SELECT 生成,后续复用 | 事务内多次读结果一致 |
sql
-- RR级别下
-- 事务B第一次SELECT → 生成Read View → 读到balance=1000
-- 事务A提交
-- 事务B第二次SELECT → 复用Read View → 仍然读到balance=1000(可重复读)
-- RC级别下
-- 事务B第一次SELECT → 生成Read View → 读到balance=1000
-- 事务A提交
-- 事务B第二次SELECT → 重新生成Read View → 读到balance=800(读已提交)
总结
| 维度 | 说明 |
|---|---|
| 本质 | 用版本链 + 读视图实现无锁读 |
| 核心组件 | 隐藏字段 + Undo Log + Read View |
| 优势 | 读写不互斥,高并发性能好 |
| 局限 | Undo Log 占空间;长事务导致旧版本无法回收 |
| 适用 | MySQL InnoDB 的 RC/RR 隔离级别 |