快照读读历史版本且不加锁,当前读读最新已提交版本且必加锁;区别由语句类型与隔离级别共同决定,核心在于是否生成固定Read View及是否触发行级锁。

快照读和当前读的本质区别
快照读读的是历史版本,不加锁;当前读读的是最新已提交版本,一定加锁。这不是语法层面的“要不要加 FOR UPDATE”,而是由语句类型 + 隔离级别共同决定的底层行为。
关键判断依据是:是否触发行级锁(SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT ... ON DUPLICATE KEY UPDATE 等都强制当前读),以及事务启动时是否生成固定 Read View。
容易被忽略的一点是:即使写的是普通 SELECT,只要在 REPEATABLE READ 下且事务已开启,它读到的也不是“此刻最新”,而是事务开始那一刻的快照——哪怕其他事务早已提交了新值。
READ COMMITTED 下快照读 ≈ 当前读
在 READ COMMITTED 隔离级别下,每次执行 SELECT 都会新建一个 Read View,所以普通查询看到的就是其他事务最新提交的数据。此时快照读和当前读在结果上往往一致,但机制不同:
-
SELECT是快照读:不加锁,走 MVCC,只过滤出可见版本 -
SELECT ... FOR UPDATE是当前读:加锁 + 读最新版本,还会阻塞其他事务修改该行
这意味着,在 RC 下做“先查后更”类逻辑(比如余额校验再扣款),仅靠普通 SELECT 无法防止并发覆盖——因为两次 SELECT 之间可能已有其他事务完成更新并提交。
REPEATABLE READ 下快照读与当前读结果明显不同
MySQL 默认隔离级别是 REPEATABLE READ,它的核心特点是:事务启动时生成一次 Read View,之后所有快照读都复用它。这就导致:
- 同一事务中多次执行
SELECT id FROM t WHERE id = 1,结果始终一样,哪怕其他事务已UPDATE并提交 - 但执行
SELECT ... FOR UPDATE或UPDATE时,会跳过快照,直接查最新版本并加锁 - 于是可能出现:快照读看到旧值
balance = 100,当前读却看到balance = 50(已被他人扣减)
这种不一致不是 bug,而是设计使然。如果你依赖“查到的值就是能用来更新的值”,就必须显式使用当前读语句,否则可能基于过期快照做决策。
哪些语句一定会触发当前读
以下操作无论隔离级别如何,一律走当前读、加锁、读最新版本:
SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE-
UPDATE、DELETE INSERT ... ON DUPLICATE KEY UPDATEREPLACE INTO
注意:INSERT 单纯插入新行不涉及当前读(无冲突行可锁),但遇到唯一键冲突时,InnoDB 会先对冲突行做当前读来判断是否可覆盖——这也是为什么高并发 INSERT ... ON DUPLICATE KEY UPDATE 容易出现死锁。
真正容易踩坑的地方在于:你以为自己在“读数据”,其实数据库已经悄悄加了锁;或者你以为“刚查过没问题”,却没意识到那是个快照,真实数据早已被改。


















