普通 select 不加锁也能读到“正确”数据,是因为 mvcc 通过 read view 判断行版本可见性,沿 roll_pointer 遍历 undo log 获取事务启动时的一致性快照,实现非阻塞读。

普通 SELECT 就是高并发非阻塞读的实现入口,前提是隔离级别设为 READ COMMITTED 或 REPEATABLE READ,且不显式加锁。
为什么普通 SELECT 不加锁还能读到“正确”的数据?
MVCC 不靠锁,靠的是每个事务启动时生成的 Read View,它记录了当前活跃事务 ID 列表、最小未提交事务 ID(m_ids)、最大已提交事务 ID(max_trx_id)等关键信息。当执行 SELECT 时,InnoDB 沿着每行的 DB_ROLL_PTR 向上遍历版本链,用当前事务的 Read View 去判断哪个版本可见——不是最新版,而是“对这个事务而言合法的历史版”。
这意味着:事务 A 的 SELECT 完全不碰事务 B 正在修改的行锁,B 可以继续 UPDATE 并生成新版本;A 读取的是 B 修改前的快照,互不等待。
- 可重复读(RR)下,
Read View在事务第一个快照读时创建,后续所有SELECT都复用它 → 同一事务内多次读结果一致 - 读已提交(RC)下,每次
SELECT都新建Read View→ 能看到其他事务刚提交的新版本 - 如果事务中混用了
SELECT ... FOR UPDATE,那这条语句会触发当前读,走加锁流程,MVCC 失效
哪些操作会绕过 MVCC 直接加锁?
只要语句明确要求“最新状态+排他性”,InnoDB 就放弃快照,转为当前读并加锁。这类操作不会享受 MVCC 的非阻塞优势:
-
SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE:即使没修改数据,也锁定当前最新行 -
INSERT、UPDATE、DELETE:必然修改数据,需定位最新版本并加 X 锁或 U 锁 - 任何带
WHERE条件但无法使用索引的SELECT:可能触发全表扫描+隐式锁升级,间接影响并发
注意:UPDATE t SET x = x + 1 WHERE id = 123 先要读 id=123 的最新值(当前读),再更新——这里第一步就跳出了 MVCC 快照读逻辑。
如何验证某次 SELECT 是否走了 MVCC?
最直接的方式是结合 INFORMATION_SCHEMA.INNODB_TRX 和 INFORMATION_SCHEMA.INNODB_LOCKS(MySQL 5.7+)观察锁持有情况:
- 开两个事务:事务 A 执行
UPDATE t SET v = 10 WHERE id = 1但不提交;事务 B 执行SELECT * FROM t WHERE id = 1 - 查
INNODB_TRX:事务 B 的TRX_STATE应为RUNNING,且TRX_ROWS_LOCKED为 0 - 查
INNODB_LOCKS(如存在):事务 B 不应出现在锁列表中;事务 A 占有一条 X 锁记录
若事务 B 出现锁等待或 TRX_STATE 是 LOCK WAIT,说明它没走快照读——大概率是隔离级别设成了 READ UNCOMMITTED 或用了覆盖索引失效的写法,导致优化器选错执行路径。
真正容易被忽略的点在于:MVCC 的有效性高度依赖事务粒度和语句写法。长事务会拖住 Read View 中的 m_ids,让大量旧版本无法被 purge,最终撑爆 undo log 空间;而一个看似简单的 SELECT COUNT(*) 如果没走索引,也可能因隐式锁行为破坏并发预期。











