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 不直接覆盖旧数据,而是:

  1. 将修改前的行写入 undo log
  2. 更新当前行的 DB_TRX_ID 和 DB_ROLL_PTR
  3. 通过 DB_ROLL_PTR 形成版本链表
当前行(trx_id=103)→ undo(102) → undo(101) → undo(100) → ...

DELETE 是标记删除位 + 写入 undo;INSERT 无旧版本。

3. ReadView(读视图 / 快照)

事务执行快照读时生成 ReadView,记录:

字段含义
m_ids生成 ReadView 时所有活跃(未提交) 的事务 ID 列表
min_trx_idm_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 InnoDBPostgreSQL
旧版本存储undo log(回滚段)旧版本留在堆表(Heap)
隐藏字段DB_TRX_ID、DB_ROLL_PTRxmin、xmax
更新方式原地更新 + undo 链写新行版本,旧行标记不可见
空间回收undo purgeVACUUM 清理死元组
默认隔离级别REPEATABLE READREAD 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 → 表膨胀

相关