📌 今日关键词:MVCC、InnoDB、并发控制、事务隔离级别、Undo Log、Read View、无锁读
大家好,我是数据库的小学妹👋
我们之前学过事务和锁,知道锁可以保证并发数据的一致性。但是你有没有想过:
如果每次读数据都要加锁,那高并发下读操作也会互相阻塞,性能岂不是很差?
为什么我们平时写SELECT从来不加锁,也不会读到错误的数据?
这就是MVCC(多版本并发控制) 的功劳。它是InnoDB引擎的“秘密武器”,让读操作不加锁也能读到一致性数据,大大提升了并发性能。
今天,我们就来揭开这个让数据库性能起飞的核心黑科技——MVCC(多版本并发控制)。让你理解数据库“无锁读”的魔法!
🧩 一、 什么是MVCC?
MVCC(Multi-Version Concurrency Control,多版本并发控制) 是一种不用加锁就能实现事务隔离的机制。它的核心思想是:
每次写操作(
UPDATE、DELETE)不直接覆盖旧数据,而是产生一个新版本。读操作根据事务的隔离级别和时间戳,选择合适的版本来读。
这样一来,读操作永远不用等待写操作,写操作也不用等待读操作,读写互不阻塞。
💡 类比:你在写一份文档,每隔一段时间保存一个历史版本。别人想读的时候,可以选择读最新版,也可以选择读某个历史版。你写你的,他读他的,互不干扰。
⚙️ 二、MVCC的底层原理:三个隐藏字段 + undo log
📍隐藏字段(The Hidden Markers)
InnoDB在每行数据背后,偷偷加了几个“看不见的字段”:
- DB_TRX_ID: “出生证明”。记录是哪个事务(Transaction)创建了这一行。
- DB_ROLL_PTR: “回溯指针”。指向这条数据修改前的“上一个版本”在哪里(指向Undo Log)。
- DB_ROW_ID: 单调递增的行ID(如果没有主键,InnoDB就用这个)。
📍Undo Log(时光隧道)——MVCC的物理实现基础
- 每当你更新一行数据,InnoDB不会直接覆盖旧数据,而是把旧数据扔进 Undo Log。
- 这样,新版本和旧版本就连成了一条“版本链”。最新的数据在链头,越往后越古老。
📍Read View(读视图)——MVCC的“决策大脑”
当执行SELECT查询时,InnoDB会生成一个Read View(读视图),它就像一个"快照",记录了当前系统中活跃事务的ID列表。
Read View 包含几个关键信息:
m_ids:生成Read View时,所有活跃的(未提交)事务ID列表min_trx_id:m_ids中的最小值max_trx_id:下一个要分配的事务ID
可见性规则(判断版本链中的哪个版本对当前事务可见):
- 当前版本的事务ID <
min_trx_id→ 可见(该事务已提交) - 当前版本的事务ID >
max_trx_id→ 不可见(该事务在未来生成,还没发生) - 当前版本的事务ID在
m_ids中 → 不可见(该事务活跃未提交,应读旧版本) - 否则 → 可见
如果当前版本不可见,就通过DB_ROLL_PTR找上一个版本,重复判断,直到找到可见版本。
💡 这就是为什么在可重复读(RR) 级别下,同一个事务多次
SELECT看到的数据是一致的——因为只读第一次生成的Read View。而读已提交(RC) 级别每次SELECT都重新生成Read View,所以能看到其他事务已提交的新数据。
🧠 三、 深度解析:为什么它能解决“读写冲突”?
结合我们之前学的“事务隔离级别”,MVCC主要解决了“读已提交(Read Committed)”和“可重复读(Repeatable Read)”这两个级别下的并发问题。
🔍场景模拟:
假设你在淘宝查看余额(事务A),同时银行正在给你发工资(事务B)。
🔶传统锁机制:
银行发工资时,你的查询必须等待,直到工资发完你才能看到余额。你得干等。
🔷MVCC机制:
- 你的查询(事务A)开始,生成了一个 Read View。
- 此时银行(事务B)还没提交,ID在Read View的“黑名单”里。
- 数据库顺着“版本链”往上找,给你展示的是发工资之前的余额快照。
- 结果: 你看数据的时候,银行在改数据,我们互不干扰!你看到的是“旧但一致”的数据,而不是错误的中间状态。
⚠️ 核心误区纠正:
很多人认为MVCC能解决“幻读”。
- 真相: 在MySQL的InnoDB中,MVCC配合Next-Key Lock(一种特殊的锁)才能彻底解决幻读。单纯靠MVCC是不够的。这也是为什么我们在之前的“锁机制”文章中提到InnoDB的锁很复杂的原因。
🚀 四、 新手避坑与实战建议
理解了MVCC,作为初级开发者,我们在写SQL时要注意什么?
💢快照读 vs 当前读
| 类型 | 语法 | 读的是什么 | 是否加锁 |
| -------- | -------------------------------------------------------- | ------------------------------ | ---------- |
| 快照读 | SELECT ... | 根据隔离级别读对应的历史版本 | 不加锁 |
| 当前读 | SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE | 读最新的数据版本 | 加锁 |
快照读就是MVCC的体现,不阻塞任何写操作。
当前读需要加锁,确保读到的是最新数据,防止并发修改。
-- 快照读(无锁)
SELECT * FROM users WHERE id = 1;
-- 当前读(加行级排他锁)
SELECT * FROM users WHERE id = 1 FOR UPDATE;
💢Undo Log会膨胀
- 因为MVCC要保留历史版本,所以 Undo Log 会越来越大。
- 避坑: 如果你的业务中有长事务(一个事务开启很久不提交),数据库为了保证你能读到那个“旧快照”,就必须一直保留那些历史版本,不能清理Undo Log。这会导致数据库空间暴涨,甚至拖垮性能。
- 想知道你的数据库里有没有‘隐形炸弹’吗?运行这条命令查一查:
SHOW ENGINE INNODB STATUS\G,重点关注TRANSACTIONS部分。
💢索引维护成本
- 虽然MVCC让读很快,但频繁的更新(UPDATE)会产生大量的历史版本,占用磁盘空间,并增加垃圾回收(Purge)的负担。
- 建议: 在高并发写入的场景下,虽然MVCC很牛,但也要控制更新的频率,能用状态机的尽量用状态机,避免无意义的频繁更新。
📌 五、 总结
今天我们把数据库最核心的并发控制机制拆解了一遍:
- MVCC 是通过保存数据的多版本来实现并发。
- 它依靠 Undo Log 形成版本链,依靠 Read View 来判断可见性。
- 它让读操作变得极其轻量,不需要等待写操作,是MySQL高性能的基石。
一句话总结: MVCC就是数据库里的“平行宇宙”,让你在一个事务里看到一个稳定不变的数据世界,而别人在另一个宇宙里改数据,互不干扰。
👋 我是数据库小学妹一个用设计师思维学数据库的转行人。我们一起,把复杂的技术变得简单有趣!💕
本文示例基于 MySQL 8.0 + InnoDB。MVCC是InnoDB核心特性,理解它对掌握事务隔离级别很有帮助。