快照读不加任何锁,是因为它通过readview和undo log读取事务可见的历史版本,而非最新行记录;在rr级别复用readview实现可重复读,rc级别每次select新建readview,仅普通select为快照读,带锁提示或dml则触发加锁的当前读。

快照读不加任何锁,但依赖Read View判断可见性
快照读本质是“无锁读”,SELECT 语句只要没带 FOR UPDATE 或 LOCK IN SHARE MODE,就走快照读路径。它不申请行锁、间隙锁或临键锁,因此不会阻塞其他事务的写操作,自己也不会被写操作阻塞。
但它不是“随便读”——是否能读到某条记录,取决于事务启动时生成的 Read View 和该记录的 undo log 版本链。比如在 REPEATABLE READ 下,事务第一次 SELECT 后,后续所有快照读都复用同一个 Read View,哪怕其他事务已提交新值,也看不到。
- RC 隔离级别下:每次
SELECT都新建Read View,所以快照读看到的是“最新已提交版本”,但依然不加锁 - RR 隔离级别下:事务内所有快照读共用初始
Read View,可能读到旧值,且完全不触发行锁 - 即使 WHERE 条件命中索引,快照读也绝不会加
gap lock或next-key lock
当前读必须加锁,且锁类型由语句决定
当前读不是“选择加锁”,而是“强制加锁”。只要触发当前读(比如 UPDATE、DELETE、SELECT ... FOR UPDATE),InnoDB 就会先定位到最新已提交的行版本,然后立即加锁——不管隔离级别是 RC 还是 RR,这个行为不变。
锁的粒度和类型取决于操作语义:
-
SELECT ... FOR UPDATE→ 加X 锁(排他锁),阻塞其他事务的SELECT ... FOR UPDATE、UPDATE、DELETE -
SELECT ... LOCK IN SHARE MODE→ 加S 锁(共享锁),允许其他事务加 S 锁,但阻塞 X 锁 -
UPDATE/DELETE→ 先当前读取目标行,再加X 锁;若涉及范围条件(如WHERE age > 25),还会加gap lock或next-key lock -
INSERT ... ON DUPLICATE KEY UPDATE→ 对冲突的唯一键行做当前读并加X 锁,这是高并发下死锁的常见源头
为什么普通 SELECT 在 RR 下读不到最新提交的数据?
这不是 bug,也不是锁没加对,而是设计使然:RR 隔离级别的核心保障是“可重复读”,靠的是事务启动时固化一个 Read View。此时 SELECT 是快照读,只按这个视图过滤版本链,根本不会去查数据页上最新的 DB_TRX_ID。
举个典型陷阱:
- 事务 A 开启后执行
SELECT balance FROM account WHERE id = 1,读到balance = 100 - 事务 B 执行
UPDATE account SET balance = 50 WHERE id = 1并COMMIT - 事务 A 再执行同样
SELECT,仍读到100(快照读复用旧Read View) - 但如果事务 A 改用
SELECT ... FOR UPDATE,就会跳过快照,直接读到50并加X 锁
这意味着:依赖“查到的值”去做业务逻辑(比如余额校验)时,SELECT 本身不提供数据新鲜度保证,必须显式升级为当前读。
加锁不是目的,而是读取最新版本的必要代价
当前读加锁的根本原因,不是为了“防止别人读”,而是为了“确保自己读到的是最新已提交版本”。InnoDB 必须通过锁来串行化访问,才能避免在读取瞬间被其他事务覆盖——比如你 SELECT ... FOR UPDATE 时,另一事务正要 UPDATE 同一行,不加锁就可能读到中间态或未提交数据。
容易忽略的关键点:
- 快照读的“不加锁”是绝对的:哪怕在唯一索引上精确匹配,也不加任何锁
- 当前读的“加锁”是刚性的:即使
WHERE条件没命中任何行(如id = 999不存在),InnoDB 仍会在对应间隙加gap lock(RR 下),防止幻读 - 锁等待超时抛出的错误是
ERROR 1205 (HY000): Deadlock found when trying to get lock或ERROR 1205 (HY000): Lock wait timeout exceeded,而不是 MVCC 相关错误
真正复杂的地方在于:同一句 SQL 在不同隔离级别、不同执行上下文(是否在事务中)、不同索引条件下,可能触发快照读或当前读——而用户往往只看语法,不看底层机制。











