MVCC(多版本并发控制)
→ 返回 数据库基础
MVCC(Multi-Version Concurrency Control,多版本并发控制)是关系型数据库实现高并发的核心机制:为每行数据维护多个版本,读操作读取某个快照,写操作创建新版本,从而让读不阻塞写、写不阻塞读。
事务 ACID 中的隔离性 largely 由 MVCC + 锁共同保证,详见 数据库事务。
为什么需要 MVCC
没有 MVCC 时,并发读写只能依赖锁:
读事务加 S 锁 → 写事务等 S 锁释放 → 并发低
写事务加 X 锁 → 读事务等 X 锁释放 → 并发低
MVCC 让普通 SELECT(快照读)不加锁,通过版本链判断可见性,大幅提升读并发。
| 读类型 | 是否加锁 | 说明 |
|---|---|---|
| 快照读 | 否 | 普通 SELECT,读历史可见版本 |
| 当前读 | 是 | SELECT ... FOR UPDATE、INSERT、UPDATE、DELETE,读最新已提交版本并加锁 |
InnoDB MVCC 三要素
1. 隐藏字段
InnoDB 每行额外存储:
| 字段 | 说明 |
|---|---|
DB_TRX_ID | 最后一次修改该行的事务 ID(单调递增) |
DB_ROLL_PTR | 指向 undo log 中上一个版本的指针 |
DB_ROW_ID | 无主键时自动生成的隐藏行 ID |
2. undo log 版本链
UPDATE 不直接覆盖旧数据,而是:
- 将修改前的行写入 undo log
- 更新当前行的
DB_TRX_ID和DB_ROLL_PTR - 通过
DB_ROLL_PTR形成版本链表
当前行(trx_id=103)→ undo(102) → undo(101) → undo(100) → ...
DELETE 是标记删除位 + 写入 undo;INSERT 无旧版本。
3. ReadView(读视图 / 快照)
事务执行快照读时生成 ReadView,记录:
| 字段 | 含义 |
|---|---|
m_ids | 生成 ReadView 时所有活跃(未提交) 的事务 ID 列表 |
min_trx_id | m_ids 中最小 ID |
max_trx_id | 下一个待分配的事务 ID(即当前最大 ID + 1) |
creator_trx_id | 创建该 ReadView 的事务 ID |
可见性判断规则
对版本链上的每个版本,按 DB_TRX_ID(记为 trx_id)判断:
if trx_id == creator_trx_id:
→ 自己修改的 → 可见
elif trx_id < min_trx_id:
→ ReadView 创建前已提交 → 可见
elif trx_id >= max_trx_id:
→ ReadView 创建后才开启 → 不可见,沿 undo 找旧版本
elif trx_id in m_ids:
→ 活跃未提交事务 → 不可见,沿 undo 找旧版本
else:
→ ReadView 创建时已提交 → 可见
沿 undo 链找到第一个可见版本即为快照读结果;找不到则该行对当前事务不可见。
RC 与 RR 的差异(MySQL InnoDB)
| 隔离级别 | ReadView 生成时机 | 效果 |
|---|---|---|
| READ COMMITTED | 每次 SELECT 都生成新 ReadView | 能读到其他事务新提交的数据 → 不可重复读 |
| REPEATABLE READ(默认) | 事务第一次快照读时生成,之后复用 | 整个事务看到同一快照 → 避免不可重复读 |
-- 查看隔离级别
SELECT @@transaction_isolation;
-- 会话级切换
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;RR 如何抑制幻读
InnoDB 在 RR 下:
- 快照读:ReadView 固定,同一事务内多次范围查询结果一致
- 当前读:配合 Next-Key Lock(行锁 + 间隙锁)阻止其他事务在间隙中插入 → 抑制幻读
详见 锁机制。
快照读 vs 当前读
-- 快照读(不加锁)
SELECT * FROM orders WHERE user_id = 1;
-- 当前读(加 X 锁)
SELECT * FROM orders WHERE user_id = 1 FOR UPDATE;
-- 当前读(加 S 锁)
SELECT * FROM orders WHERE user_id = 1 LOCK IN SHARE MODE;
-- DML 都是当前读
UPDATE orders SET status = 'paid' WHERE id = 100;
DELETE FROM orders WHERE id = 100;典型场景:先 SELECT 查库存(快照读),再 UPDATE 扣库存(当前读)—— 中间可能被其他事务修改,导致超卖。正确做法是用 SELECT ... FOR UPDATE 或乐观锁(版本号)。
MySQL vs PostgreSQL MVCC 对比
| 维度 | MySQL InnoDB | PostgreSQL |
|---|---|---|
| 旧版本存储 | undo log(回滚段) | 旧版本留在堆表(Heap) |
| 隐藏字段 | DB_TRX_ID、DB_ROLL_PTR | xmin、xmax |
| 更新方式 | 原地更新 + undo 链 | 写新行版本,旧行标记不可见 |
| 空间回收 | undo purge | VACUUM 清理死元组 |
| 默认隔离级别 | REPEATABLE READ | READ COMMITTED |
| 纯 MVCC 读 | 快照读无锁 | 普通 SELECT 无锁 |
PostgreSQL 详情见 PostgreSQL MVCC 与事务。
与 redo / undo log 的关系
| 日志 | 作用 | 服务特性 |
|---|---|---|
| undo log | 存旧版本,支持回滚 + MVCC 版本链 | 原子性、隔离性(快照读) |
| redo log | 存修改后的物理日志,崩溃恢复 | 持久性(WAL) |
UPDATE 一行:
1. 写 undo(旧版本)
2. 更新数据页(新版本)
3. 写 redo(待刷盘)
4. 提交时 redo fsync
常见面试题
1. RC 下两次 SELECT 结果不同?
RC 每次 SELECT 生成新 ReadView,能读到其他事务新提交的数据 → 不可重复读。
2. RR 下还会幻读吗?
- 快照读:不会(ReadView 固定)
- 当前读:不加锁时可能;InnoDB 用间隙锁抑制
3. MVCC 能完全替代锁吗?
不能。写操作、当前读、SERIALIZABLE 仍需要锁保证互斥。
4. 长事务有什么危害?
- 阻止 undo log 回收 → 占用大量空间
- RR 下 ReadView 长期不释放 → 旧版本堆积
- PostgreSQL 长事务阻止 VACUUM → 表膨胀
相关
- 数据库事务 — ACID、隔离级别、分布式事务
- 锁机制 — 行锁、间隙锁、Next-Key Lock
- MySQL 事务与锁
- PostgreSQL MVCC 与事务
- 索引 — 锁与索引的关系(无索引则锁升级为表锁)