一、什么是 MVCC
MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种数据库并发控制机制。它的核心思想是:为同一份数据保留多个版本,让读操作和写操作各自访问不同版本的数据,从而在不加锁(或尽量少加锁)的情况下实现并发读写。
在传统的锁机制下,读操作通常需要加共享锁,写操作需要加排他锁,读写之间会互相阻塞。而 MVCC 通过"版本"把读写解耦:读操作读取的是某个时间点的一致性快照,写操作则生成新版本,二者互不干扰。
一句话概括:MVCC 用"空间换并发",通过保存历史版本,让读不加锁、读写不阻塞。
二、MVCC 要解决的核心问题
- 读写冲突:读操作不应被写操作阻塞,写操作也不应被读操作阻塞。
- 一致性读:同一事务内多次读取同一数据,结果应保持一致(可重复读)。
- 并发性能:减少锁竞争,提升高并发场景下的吞吐量。
- 隔离级别支持:为读已提交(RC)、可重复读(RR)等隔离级别提供实现基础。
三、MVCC 的核心组成
3.1 隐藏版本字段
以 InnoDB 为例,每行记录除了用户定义的列之外,还有几个隐藏字段:
| 字段 | 含义 |
|---|---|
| DB_TRX_ID | 最近一次插入或修改该行的事务 ID |
| DB_ROLL_PTR | 回滚指针,指向 undo log 中的上一个版本 |
| DB_ROW_ID | 行 ID,当表没有主键时用于生成聚簇索引 |
3.2 undo log 版本链
每次修改数据时,旧版本会被写入 undo log,并通过 DB_ROLL_PTR 串成一条版本链。链头是最新版本,链尾是最早的历史版本。读取时根据可见性规则沿链回溯,找到对当前事务可见的版本。
3.3 Read View(读视图)
Read View 是事务在执行快照读时生成的一份"可见性判断依据",主要包含:
m_ids:生成 Read View 时活跃(未提交)的事务 ID 列表。min_trx_id:活跃事务中的最小事务 ID。max_trx_id:生成 Read View 时系统应分配的下一个事务 ID。creator_trx_id:创建该 Read View 的事务 ID。
四、可见性判断规则
拿到某个版本的 DB_TRX_ID 后,按以下顺序判断该版本是否可见:
- 若
trx_id == creator_trx_id,说明是自己修改的,可见。 - 若
trx_id < min_trx_id,说明该版本在 Read View 生成前已提交,可见。 - 若
trx_id >= max_trx_id,说明该版本在 Read View 生成后才开启,不可见。 - 若
min_trx_id <= trx_id < max_trx_id:若trx_id在m_ids中,说明事务仍活跃,不可见;否则可见。
若当前版本不可见,则沿 DB_ROLL_PTR 找到上一个版本,重复上述判断,直到找到可见版本或链结束。
五、快照读与当前读
5.1 快照读(Snapshot Read)
普通的 SELECT 属于快照读,读取的是 Read View 对应的历史版本,不加锁。
sql
-- 快照读,读取一致性快照
SELECT * FROM account WHERE id = 1;
5.2 当前读(Current Read)
读取最新已提交版本,并对读取的记录加锁,常见于更新、删除以及加锁查询。
sql
-- 当前读,加排他锁
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- 当前读,加共享锁
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE;
-- 更新语句本身也是当前读
UPDATE account SET balance = balance - 100 WHERE id = 1;
六、RC 与 RR 下 Read View 的差异
- 读已提交(RC):每次快照读都会重新生成 Read View,因此能看到其他事务已提交的最新数据,可能出现不可重复读。
- 可重复读(RR):只在事务中第一次快照读时生成 Read View,后续复用,因此同一事务内多次读取结果一致。
这也解释了为什么 InnoDB 在 RR 隔离级别下能避免不可重复读,但幻读仍需依靠间隙锁(Next-Key Lock)配合解决。
七、MVCC 的使用方式与实践建议
7.1 合理选择隔离级别
大多数业务场景使用默认的 RR 即可;对一致性要求不高、追求更高并发的场景可考虑 RC。切换隔离级别会影响 Read View 的生成时机,需要结合业务评估。
7.2 避免长事务
长事务会导致 undo log 无法及时清理,版本链变长,回滚段膨胀,同时影响 purge 线程回收历史版本。建议:
- 控制事务粒度,避免在事务中执行耗时操作(如远程调用、大批量计算)。
- 为事务设置合理的超时时间。
- 监控
information_schema.innodb_trx中的长事务。
7.3 区分快照读与当前读
在需要读取最新数据并保证一致性的场景(如扣减库存),应使用当前读或加锁语句,避免基于过期快照做业务判断。
sql
-- 扣减库存:使用当前读保证读到最新值
START TRANSACTION;
SELECT stock FROM product WHERE id = 100 FOR UPDATE;
UPDATE product SET stock = stock - 1 WHERE id = 100 AND stock > 0;
COMMIT;
7.4 注意 MVCC 不能完全替代锁
MVCC 主要解决读写冲突,写写冲突仍需依靠行锁。对于唯一性约束、范围更新等场景,仍需理解锁机制,避免死锁和幻读问题。
八、MVCC 的优缺点
| 维度 | 说明 |
|---|---|
| 优点 | 读不加锁、读写不阻塞,显著提升并发读性能;天然支持一致性读 |
| 优点 | 减少锁竞争,降低死锁概率 |
| 缺点 | 需要额外存储空间保存历史版本 |
| 缺点 | 版本链过长会增加读取开销,需要 purge 机制回收 |
| 缺点 | 实现复杂,对长事务敏感 |
九、总结
MVCC 通过隐藏版本字段、undo log 版本链和 Read View 三部分协作,实现了"读不加锁、读写不阻塞"的一致性读能力。理解可见性判断规则、快照读与当前读的区别,以及 RC 与 RR 下 Read View 的生成时机,是掌握 MVCC 的关键。在实际使用中,应重点关注隔离级别选择、长事务治理和写写冲突的锁处理,才能充分发挥 MVCC 的并发优势。