mysql读写互不阻塞由mvcc硬编码实现:快照读(普通select)不加锁、查历史版本;当前读(update/delete/select...for update)加锁、查最新行。

MySQL 的读写互不阻塞,不是靠“运气”或“配置开关”,而是由 InnoDB 在 REPEATABLE READ 和 READ COMMITTED 隔离级别下,通过 MVCC 机制硬编码实现的。核心就两点:普通 SELECT 走快照读(不加锁、查历史版本),UPDATE/DELETE/SELECT ... FOR UPDATE 等走当前读(加锁、查最新行)。一旦混淆这两类读,就会误以为“MVCC 失效”或“突然阻塞”。
快照读到底读的是哪个版本?看 Read View 怎么生成
快照读不等于“读事务开始时的全库快照”,它读的是基于当前事务启动时刻构造的 Read View 所允许访问的最新历史版本。关键差异在隔离级别:
-
READ COMMITTED:每次执行SELECT都会新建一个Read View,所以同个事务内两次SELECT可能读到不同结果(已提交的新版本) -
REPEATABLE READ:事务第一次执行SELECT时生成唯一Read View,后续所有快照读都复用它,因此能保证可重复读
判断某行版本是否可见,依赖三个值:min_trx_id(活跃事务中最小 ID)、max_trx_id(下一个将分配的事务 ID)、m_ids(生成 Read View 时所有活跃事务 ID 列表)。若某行的 DB_TRX_ID 在 m_ids 中,说明该版本由未提交事务生成,不可见。
当前读为什么一定会阻塞?因为它绕过了 MVCC
只要语句显式要求“必须读最新”,InnoDB 就放弃快照逻辑,直接走当前读路径。这类操作包括:
-
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE -
UPDATE、DELETE、INSERT ... ON DUPLICATE KEY UPDATE
它们会:① 先定位最新行(可能触发 gap lock / next-key lock);② 对目标行加锁;③ 基于最新行做修改或返回。此时如果另一事务正持有对应行锁,就会等待——这不是 MVCC “失效”,而是你主动选择了锁路径。
Undo Log 版本链不是无限的,过期版本会被 purge
每一行的 DB_ROLL_PTR 指向 Undo Log 中前一版本,形成单向链表。但旧版本不会永远保留:
- 只有仍被某个活跃
Read View引用的版本才安全; - 一旦所有事务都不再需要某个旧版本(即没有
Read View的min_trx_id小于等于该版本DB_TRX_ID),purge 线程就会异步清理它; - 如果长事务长期不提交,会拖住
Read View,导致 Undo Log 膨胀、磁盘空间耗尽、甚至影响其他事务的版本清理。
所以“读不阻塞”的代价是存储开销和 purge 压力,不是零成本。
哪些情况会让快照读“看起来像阻塞”?其实是锁升级或索引缺失
快照读本身从不加锁,但以下场景会让查询变慢甚至卡住,容易被误判为“MVCC 阻塞”:
- 没走索引的
SELECT:全表扫描过程中,仍需逐行判断每个版本是否可见,I/O 和 CPU 开销大; - 高并发下大量版本链遍历:某行被频繁更新,版本链长达数百节点,每次快照读都要从最新版往前遍历直到找到可见版本;
- 与当前读混合时的锁竞争:事务 A 在跑快照读,事务 B 对同一行执行
UPDATE并持有行锁,此时事务 C 的SELECT ... FOR UPDATE会等 B,而 A 不受影响——但监控里可能只看到“SELECT 等待”,却没注意它是带FOR UPDATE的。
真正容易被忽略的是:快照读的性能瓶颈不在锁,而在版本链长度和可见性判断开销。线上遇到慢快照读,先查 information_schema.INNODB_TRX 看有没有长事务,再用 SHOW ENGINE INNODB STATUS 观察 ROLLBACK SEGMENTS 是否堆积。











