MySQL的MVCC是InnoDB在READ COMMITTED和REPEATABLE READ下自动启用的读机制,依赖隐藏字段(DB_TRX_ID、DB_ROLL_PTR)、undo log和Read View协同实现非锁定读;Read View事务私有,REPEATABLE READ复用、READ COMMITTED每次新建;快照有效性取决于事务启动时机与undo是否被purge。

MySQL 的 MVCC 不是开关,而是 InnoDB 在 READ COMMITTED 和 REPEATABLE READ 隔离级别下自动启用的读机制;它靠隐藏字段 + undo log + Read View 三者协同实现“读不加锁、读写不互阻”——但快照是否可用,完全取决于事务启动时机和 undo 是否被清理。
DB_TRX_ID 和 DB_ROLL_PTR 这两个隐藏字段到底存什么
每行记录实际包含 DB_TRX_ID(最后一次修改该行的事务 ID)和 DB_ROLL_PTR(指向 undo log 中前一版本的指针)。这两个字段不是可配置项,InnoDB 自动维护:
-
DB_TRX_ID是事务提交时写入的,数值递增,用于判断版本可见性 -
DB_ROLL_PTR指向 update undo log 中的历史版本,构成“版本链”,最新版在链头,最早版在链尾 - 没有主键的表还会额外有
DB_ROW_ID,但仅作内部标识,不影响 MVCC 判断逻辑
Read View 是怎么生成、复用和失效的
Read View 不是全局共享的,而是每个事务私有。它的生成时机和内容直接决定你能看到哪个版本的数据:
- 事务真正启动时(即执行第一条
SELECT、UPDATE、INSERT或DELETE语句时),InnoDB 创建一个 Read View - Read View 包含四个关键字段:
m_up_limit_id(最小活跃事务 ID)、m_low_limit_id(最大事务 ID + 1)、m_ids(创建时刻所有活跃事务 ID 列表)、creator_trx_id(当前事务自己的 ID) - 在 REPEATABLE READ 下,这个 Read View 被整个事务复用;在 READ COMMITTED 下,每次
SELECT都新建一个 - 如果事务长时间未提交,导致其 ID 一直留在
m_ids中,其他事务的 undo log 就无法被 purge,可能引发Undo log space exhausted错误
为什么 SELECT 有时突然变成锁定读(当前读)
所谓“一致性非锁定读”只适用于纯 SELECT。一旦触发以下任一条件,InnoDB 就会放弃快照,转为读最新版本并加锁:
- 显式写了
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE - 执行
UPDATE或DELETE时,WHERE 条件命中某行,InnoDB 必须先做一次当前读来定位记录 - 查询走 UNIQUE 索引等值查找,且该记录正被其他未提交事务修改——此时快照读可能因“一致性检查失败”而退化为当前读
- 事务开启后很久才执行第一条
SELECT,而期间大量事务提交/回滚,导致原始 Read View 中的m_ids已不可信,InnoDB 可能拒绝提供快照并报错ER_LOCK_WAIT_TIMEOUT或直接降级
undo log 被 purge 后快照就不可用了
undo log 不是永久保存的。Purge 线程会定期清理“对所有活跃事务都不可见”的旧版本。这意味着:
- 一个长事务(比如运行超 1 小时)会让所有它启动时已存在的事务 ID 一直保留在
m_ids中 - 只要这些 ID 还在,对应版本的 undo log 就不能删;否则后续快照读会找不到历史版本,只能报错或强制当前读
-
innodb_max_purge_lag和innodb_purge_batch_size可调,但治标不治本;真正要防的是应用层避免超长事务 - 查
information_schema.INNODB_TRX表里的TRX_STARTED字段,比看PROCESSLIST更准——因为事务可能已开始但尚未发 SQL
真正容易被忽略的点是:MVCC 的“一致性”只对事务内有效,且完全依赖时间点上的系统状态;它不保证跨事务可见性,也不承诺数据物理存在——快照只是逻辑视图,背后 undo 一丢,视图就崩。











