mvcc是innodb在rc和rr隔离级别下自动启用的底层机制,仅对快照读(普通select)生效,通过readview、隐藏字段和undo log实现版本可见性控制;当前读(如select...for update)绕过mvcc并加锁,且mvcc不解决写写冲突导致的丢失更新。

MySQL 的 MVCC 不是某种开关或配置项,而是 InnoDB 引擎在 READ COMMITTED 和 REPEATABLE READ 隔离级别下自动启用的底层机制——它不靠你“开启”,但必须理解它“何时生效、为何失效、怎么被绕过”。
快照读 vs 当前读:MVCC 只对前者起作用
MVCC 仅作用于快照读(即普通 SELECT),对当前读(如 SELECT ... FOR UPDATE、UPDATE、DELETE)完全无效。后者仍会加锁,触发行锁或间隙锁。
- 快照读示例:
SELECT * FROM user WHERE id = 1→ 走 MVCC,读历史版本 - 当前读示例:
SELECT * FROM user WHERE id = 1 FOR UPDATE→ 跳过 MVCC,查最新行并加锁 - 混淆点:很多人以为“开了 RR 级别就全程 MVCC”,其实只要语句带锁,MVCC 就不参与可见性判断
ReadView 是决定“你能看到哪个版本”的关键
ReadView 不是全局统一的,它的生成时机和内容直接取决于事务隔离级别:
-
READ COMMITTED:每次执行SELECT都新建一个 ReadView,所以同个事务内两次查询可能看到不同结果(不可重复读) -
REPEATABLE READ:事务第一次执行快照读时生成 ReadView,并在整个事务中复用——这是“可重复读”的根本保障 - ReadView 包含三个核心信息:
m_ids(活跃事务 ID 列表)、min_trx_id(最小未提交事务 ID)、max_trx_id(下一个将分配的事务 ID) - 判断某行是否可见的逻辑是:
DB_TRX_ID必须小于min_trx_id,或在m_ids中不存在,且不等于当前事务 ID
隐藏字段 + Undo Log 构成版本链的物理基础
InnoDB 每行数据背后实际存着三个隐藏字段,它们不是可选功能,而是 MVCC 运转的硬依赖:
-
DB_TRX_ID:记录最后一次修改该行的事务 ID(插入/更新/删除都更新它) -
DB_ROLL_PTR:7 字节指针,指向 Undo Log 中该行的上一个版本;多个版本通过这个指针串成链表 -
DB_ROW_ID:仅当表无主键/唯一非空索引时由 InnoDB 自动生成,与 MVCC 无关 - Undo Log 不是日志文件,而是存储在 rollback segment 中的结构化数据页;版本链越长,回溯开销越大,尤其在长事务未提交时会阻塞 purge 线程清理旧版本
为什么 MVCC 无法解决“更新丢失”?
MVCC 解决的是读写冲突,不是写写冲突。两个事务同时更新同一行,必然发生覆盖——InnoDB 用锁来处理,而不是版本。
- 事务 A 执行
UPDATE t SET v = v + 1 WHERE id = 1,会先做当前读,加 X 锁;事务 B 同时执行同样语句,会被阻塞直到 A 提交或回滚 - 即使 A 和 B 都基于相同快照读出 v=100,各自算出 101 后写入,也只会有一个成功;后提交者要么失败(锁超时),要么覆盖(取决于执行顺序)
- 想避免更新丢失,得靠应用层重试 + 版本号(
WHERE version = ?)或数据库层的SELECT ... FOR UPDATE显式加锁
真正容易被忽略的是:MVCC 的“多版本”不免费——每行额外 13 字节存储开销,Undo Log 持续增长会拖慢 purge 效率,而长时间未提交的事务会让 ReadView 一直保留旧 min_trx_id,导致大量历史版本无法清理。这不是理论风险,是线上慢查询和磁盘暴涨的常见根因。











