普通select在rc/rr下默认是快照读,根本原因是innodb通过mvcc实现一致性非锁定读:rc每次生成新readview,rr复用首次readview,均依赖undo log回溯历史版本而非加锁。

普通SELECT在RR/RC下默认是快照读
MySQL的InnoDB引擎对普通SELECT不加锁,根本原因在于它默认走的是**一致性非锁定读(consistent non-locking read)**,也就是快照读。这个行为由MVCC机制支撑,和事务是否“已开启”无关,只取决于隔离级别和语句类型。
在READ COMMITTED下,每次SELECT生成新的ReadView;在REPEATABLE READ(InnoDB默认)下,事务内第一个SELECT生成ReadView,后续复用——但无论哪种,都不需要加锁去阻塞别人,而是靠undo log回溯历史版本来保证可见性。
所以你执行BEGIN后再跟一条SELECT * FROM t WHERE id = 1,它只是读快照,不是查最新行,自然不申请S锁或X锁。
只有显式加锁或特定场景才触发行锁
真正会加锁的SELECT必须带锁提示,或者触发隐式锁逻辑:
-
SELECT ... FOR UPDATE→ 加排他锁(X锁) -
SELECT ... LOCK IN SHARE MODE→ 加共享锁(S锁) - 在
SERIALIZABLE隔离级别下,所有普通SELECT自动等价于LOCK IN SHARE MODE - 外键检查、唯一键冲突检查、
INSERT ... SELECT中的源表扫描,也可能隐式加S锁
注意:SELECT INTO @var属于读写操作,会启动事务并可能加锁;而纯SELECT哪怕在autocommit = 0模式下,只要没锁提示,就不加锁。
autocommit=0后第一条DML才真正开启事务
很多人误以为SET @@autocommit = 0之后就“进事务了”,其实不是:
- 此时执行
SELECT仍不启动事务,也不加锁 - 直到第一条可执行DML(如
INSERT、UPDATE、DELETE)或带锁SELECT执行时,InnoDB才隐式开启事务 - 这意味着:在
SET @@autocommit = 0和第一条DML之间的SELECT,既不在事务中,也不受事务隔离保护——它读的是当前最新已提交版本,但不建立ReadView
这个空档期最容易被忽略,尤其在调试时看到“结果不对”,却没意识到那条SELECT根本没进事务上下文。
如何验证某条SELECT是否加锁
不能只看SQL长得像不像锁语句,得看实际锁行为:
- 查当前锁:
SELECT * FROM performance_schema.data_locks WHERE THREAD_ID = CONNECTION_ID() - 查锁等待:
SELECT * FROM sys.innodb_lock_waits - 开启
innodb_status_output_locks后,用SHOW ENGINE INNODB STATUS看详细锁信息 - 在另一个会话里尝试
UPDATE同一条记录,如果被阻塞,说明前一个SELECT确实加了锁(比如用了FOR UPDATE)
记住:没报错、没阻塞、data_locks里没记录,基本就能断定那条SELECT没加锁——哪怕它在BEGIN之后。
事务真正开始的时机和锁是否启用,是两件独立的事。前者看DML或显式锁语句,后者看语句类型+隔离级别+是否带锁提示。混淆这两点,是线上出现“读到旧数据”或“莫名被阻塞”的常见源头。











