mysql的select默认是快照读、不加锁,但仅限rr隔离级别下事务首次查询且未触发当前读;一旦涉及索引扫描、锁提示或后续更新,便会退化为加锁的当前读。

SELECT 不加锁?先看是不是快照读
MySQL 的 SELECT 默认不加锁,但前提是它走的是**快照读(snapshot read)**——也就是在可重复读(RR)隔离级别下,通过 MVCC 读取事务开始时的已提交版本。这时候你看到的数据可能不是最新提交的,也不会阻塞其他事务写入。
容易踩的坑是:以为“没写 FOR UPDATE 就一定不锁”,结果发现某条 SELECT 突然卡住别人更新,或者自己被锁住。那大概率它已经退化成当前读了。
- 触发当前读的常见操作:
SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT - 即使没显式加锁,如果
SELECT出现在一个已开启的事务中,且后续要执行UPDATE,优化器有时会提前按当前读处理(尤其带WHERE条件命中索引时) - 唯一能确保快照读的方式:事务内首次
SELECT后,不再执行任何当前读语句;否则 MVCC 视图可能刷新
RR 隔离级别下,普通 SELECT 为什么有时会锁表
这不是 MySQL “坏了”,而是你误触了**隐式当前读场景**。典型例子是:在 RR 下执行 SELECT * FROM t WHERE id = 10,而 id 是非唯一索引或没索引,InnoDB 为了保证可重复读语义,可能对搜索路径上的所有记录(甚至间隙)加临键锁(next-key lock)——但注意:这是当前读才有的行为,普通快照读不会。
真正让普通 SELECT 变成锁表的,往往是下面这些情况:
- 查询条件无法使用索引(全表扫描 + 当前读),比如
SELECT * FROM user WHERE name LIKE '%张%'在事务中配合UPDATE后续操作 - 使用了
SELECT ... FOR UPDATE或LOCK IN SHARE MODE却忘了删掉,调试完忘了还原 - ORM 框架自动注入了锁提示,比如 Django 的
.select_for_update()、MyBatis 的forUpdate="true" - MySQL 8.0+ 中启用
innodb_locks_unsafe_for_binlog=ON(已废弃)或低版本中 gap locking 被禁用,反而导致锁范围异常扩大
如何快速判断一条 SELECT 到底加没加锁
别猜,用 INFORMATION_SCHEMA.INNODB_TRX 和 INFORMATION_SCHEMA.INNODB_LOCKS(MySQL 5.7)或 performance_schema.data_locks(8.0+)直接查。最简单有效的现场验证方式:
- 开两个会话:会话 A 执行
BEGIN; SELECT ...(带或不带锁提示);会话 B 立即尝试UPDATE同一行;看是否阻塞 - 在会话 A 中执行后,立刻查
SELECT * FROM performance_schema.data_locks,观察LOCK_DATA和LOCK_MODE字段 ——S,REC_NOT_GAP表共享记录锁,X,REC_NOT_GAP是排他记录锁,X,GAP是间隙锁 - 用
EXPLAIN FORMAT=tree看执行计划里有没有Using where; Using index,有则大概率走索引快照读;若出现Using temporary; Using filesort,又在事务里,就要警惕是否触发了当前读路径
READ-COMMITTED 下 SELECT 的锁行为更“实在”
RC 隔离级别下,快照读的视图是“语句级”的,每次 SELECT 都读最新已提交版本;当前读依然加锁,但只锁匹配到的记录,不锁间隙(gap locking 被禁用)。这意味着:
- 幻读可能发生(因为不锁间隙),但锁冲突概率下降,适合高并发读多写少场景
-
SELECT ... FOR UPDATE在 RC 下只锁实际查到的行,不会像 RR 那样顺手把前后间隙也锁住 - 如果你的应用依赖“多次 SELECT 结果一致”,却用了 RC,那不是锁的问题,是隔离级别选错了
- 从 RR 切到 RC 前,务必确认业务能接受不可重复读 —— 比如报表类查询突然多出/少了一条记录,可能比锁等待更难排查
复杂点在于:锁行为不是由单条语句决定的,而是事务上下文 + 隔离级别 + 索引结构 + 查询条件共同作用的结果。最容易被忽略的,其实是事务里第一条语句之后的每一条 SQL,都在悄悄影响着 MVCC 视图和加锁策略。











