快照读不加任何锁,仅依赖ReadView和undo log版本链判断可见性;当前读则强制加锁读取最新已提交行,锁行为与隔离级别无关。

快照读不加锁,但依赖 ReadView 和 undo log 生存
快照读(比如 SELECT * FROM t WHERE id = 1)根本不会触发行锁、间隙锁,甚至不申请意向锁。它只靠事务启动时生成的 ReadView,结合每行的 DB_TRX_ID 和 DB_ROLL_PTR 去 undo log 版本链上找可见版本。这意味着:
- 事务 A 执行快照读时,事务 B 正在更新同一行并没提交,A 不会等,也不会被阻塞
- 事务 B 已提交新值,A 在 RR 级别下仍看不到——不是锁没释放,是
ReadView没更新 - 如果事务 A 长时间不提交,它的
ReadView就一直有效,undo log 无法被 purge 清理,undo 表空间可能暴涨
这个机制不是“松”,而是“静默”:无锁,但强依赖 undo log 存活。线上最常踩的坑是监控只看锁等待,却忽略了 undo 增长和版本链遍历变慢。
当前读强制走锁路径,与隔离级别无关
SELECT ... FOR UPDATE、UPDATE、DELETE 这些操作一律触发当前读,InnoDB 会跳过 MVCC 版本链,直接定位聚簇索引上最新已提交的物理行,然后加锁。关键点在于:
- RR 和 RC 下行为完全一致:都加 X/S 锁,都可能加 gap lock 或 next-key lock
- 是否走索引影响极大:
WHERE id = 1(主键命中)只锁单行;WHERE age > 25(无索引)会全表扫描+全区间加锁,极易引发阻塞 -
INSERT ... ON DUPLICATE KEY UPDATE是特例:对冲突行做当前读并加 X 锁,对新插入行只加插入意向锁,死锁风险高
很多人误以为“RC 下当前读锁得少”,其实只是快照读可见性不同,当前读的锁行为在两种级别下毫无区别。
区分语句类型比记隔离级别更重要
真正决定读行为的是 SQL 写法,不是你设了什么隔离级别。以下规则可直接套用:
- 纯
SELECT(无任何锁提示)→ 快照读 -
SELECT ... FOR UPDATE/SELECT ... LOCK IN SHARE MODE→ 当前读 - 所有 DML(
INSERT、UPDATE、DELETE)→ 隐式当前读(先读最新行,再加锁操作)
哪怕你在 RC 级别执行 SELECT * FROM t WHERE id = 1 FOR UPDATE,它依然加 X 锁、读最新行、阻塞其他事务的当前读;而同一条语句去掉 FOR UPDATE,就立刻变成无锁快照读。别被隔离级别带偏判断方向。
幻读问题只在当前读场景暴露
MVCC 的快照读天然防幻读:同一个事务内多次 SELECT * FROM t WHERE id > 100,结果集始终一致(RR 下复用 ReadView)。但一旦换成当前读,比如:
SELECT * FROM t WHERE id > 100 FOR UPDATE;
就可能在第二次执行时发现新插入的满足条件的行,并对其加锁——这就是幻读。InnoDB 不靠 MVCC 解决这个问题,而是用间隙锁(gap lock)或 next-key lock 来封锁区间。所以:
- 幻读是否发生,取决于你用的是快照读还是当前读,而不是隔离级别本身
- RR 级别下 gap lock 是默认开启的,但若查询条件没走索引,InnoDB 可能退化为表级锁,反而更危险
最容易被忽略的一点:你写的业务逻辑里,看似只读的 SQL,只要加了 FOR UPDATE,就已脱离 MVCC 保护范围,进入锁竞争世界——这时候查慢、死锁、锁等待,全都跟快照读无关。


















