mvcc是innodb在read committed和repeatable read级别下为普通select实现的一致性快照读机制,依赖db_trx_id和db_roll_ptr等隐藏字段与readview协同判断版本可见性,非锁的替代品。

什么是MVCC:它不是“锁的替代品”,而是“快照读的实现方式”
MVCC不是一种独立运行的机制,它是InnoDB在READ COMMITTED和REPEATABLE READ隔离级别下,为普通SELECT语句自动启用的一致性读方案。它不干预UPDATE、DELETE或带FOR UPDATE的查询——这些仍是当前读,要加锁。
关键点在于:
-
SELECT * FROM t WHERE id = 1→ 快照读,走MVCC,不加锁 -
SELECT * FROM t WHERE id = 1 FOR UPDATE→ 当前读,跳过MVCC,直接查最新版本并加行锁
你不能“手动开启MVCC”,它由隔离级别+语句类型隐式触发。误以为“用了MVCC就全程无锁”,是线上死锁或数据不一致的常见起点。
三个隐藏字段怎么参与读取判断:DB_TRX_ID 和 DB_ROLL_PTR 是核心
InnoDB每行数据背后实际存着至少两个隐藏字段:
-
DB_TRX_ID:最后一次修改这行的事务ID(6字节) -
DB_ROLL_PTR:指向Undo Log中上一个版本的指针(7字节) -
DB_ROW_ID:仅在无主键表中用作隐式主键(可忽略)
当你执行一次快照读时,InnoDB会:
- 先拿到当前事务的
ReadView(含活跃事务列表m_ids、最小/最大事务ID等) - 检查当前行的
DB_TRX_ID是否在m_ids里(未提交 → 不可见) - 若不可见,就顺着
DB_ROLL_PTR往Undo Log里找上一个版本,再比对 - 直到找到一个
DB_TRX_ID小于min_trx_id(已提交且早于当前事务启动)的版本,返回它
这就是为什么RR级别下同一事务内多次SELECT结果一致:ReadView只在第一次读时生成,后续复用,版本链遍历起点不变。
ReadView生成时机决定隔离效果:RC每次读都新建,RR只建一次
这是理解“不可重复读”和“可重复读”差异的关键分水岭:
- 在
READ COMMITTED下:每次SELECT都会新建一个ReadView,所以其他事务刚提交的修改,下一次查询就能看到 - 在
REPEATABLE READ下:事务中第一次快照读时生成ReadView,之后所有快照读都复用它,哪怕其他事务已提交,只要其ID不在初始m_ids里,就永远不可见
典型陷阱:
- 你在RR事务里先
SELECT查到某条记录,然后另一个事务UPDATE并提交了它 - 你再
SELECT,看到的仍是旧值 → 这不是bug,是MVCC按设计工作 - 但如果你接着做
UPDATE ... WHERE id = X,InnoDB会执行当前读,查最新版本并加锁,这时就可能发现“值已变”,甚至触发唯一键冲突或更新丢失
Undo Log版本链不是无限存的:Purge线程清理滞后会导致空间暴涨
Update Undo Log必须保留,直到确认没有任何活跃事务还需要访问其中的版本。但Purge线程是异步的,且默认不激进清理。
后果很实际:
- 长事务(比如没提交的
BEGIN; SELECT ...;挂了几小时)会让m_ids长期包含旧ID,阻塞Purge -
information_schema.INNODB_METRICS里undo_log_pages_written持续上涨,innodb_undo_log_truncate没生效 - 最终
ibdata1或独立undo表空间膨胀,磁盘告警
应对动作:
- 监控
SELECT TRX_ID, TRX_STARTED, TRX_STATE FROM information_schema.INNODB_TRX,杀掉超时空闲事务 - 设置
innodb_max_purge_lag软限流,避免Purge跟不上写入速度 - 避免在RR级别下写“先查后更”的业务逻辑,改用
SELECT ... FOR UPDATE显式加锁控制版本可见性
MVCC让读不阻塞写,但也把“版本可见性”的判断逻辑从锁竞争转移到了内存遍历和Undo空间管理上——后者更容易被忽视,却在高写入+长事务场景下成为性能拐点。











