select是否加锁取决于隔离级别和索引:rr下普通select为快照读不加锁,但select...for update必加锁;有索引时加行级锁,无索引可能锁全表。

SELECT 不加锁?得看隔离级别和有没有索引
MySQL 的锁行为不是由 SQL 类型绝对决定的,而是和事务隔离级别、语句是否走索引、执行计划强相关。比如 SELECT * FROM t WHERE id = 1 在 RR(可重复读)下,如果 id 是主键,InnoDB 会加 **行级记录锁(Record Lock)**;如果 id 没索引,就会退化为 **表级意向锁 + 间隙锁或临键锁的组合**,甚至全表扫描时锁住所有聚簇索引页。
常见误区是认为 “SELECT 不锁表”,其实只在 RC(读已提交)下搭配唯一索引查询才可能不加锁(快照读),但一旦用了 SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE,不管什么级别都立刻加锁。
- RR 下普通
SELECT是快照读,不加锁(但背后有 MVCC 版本链维护) - RC 下普通
SELECT也是快照读,不过只保证语句级一致性,不保证事务内一致性 - 任何
SELECT ... FOR UPDATE都会触发当前读,并加记录锁或临键锁 - 没走索引的
WHERE条件,即使只是SELECT ... FOR UPDATE,也可能锁全表(实际是锁所有扫描到的索引页)
UPDATE/DELETE 加的是临键锁(Next-Key Lock),不只是行锁
InnoDB 默认在 RR 隔离级别下对范围条件使用 **临键锁(Next-Key Lock)**,即“记录锁 + 间隙锁”的合体。它既锁住匹配的记录,也锁住该记录前的间隙,防止幻读。比如 UPDATE t SET name='x' WHERE age > 20,哪怕 age 有索引,也会锁住所有满足 age > 20 的记录及其右侧间隙。
这个设计常被低估:你以为只改几行,实际上可能锁住一大片索引范围,导致其他事务在相邻值上插入/更新被阻塞。
- 唯一索引等值查询(如
WHERE id = 100)→ 只加记录锁(Record Lock) - 非唯一索引等值查询(如
WHERE name = 'a')→ 加临键锁(锁该值+前间隙) - 范围查询(
>=、BETWEEN、LIKE 'abc%')→ 加临键锁,覆盖整个扫描区间 -
DELETE和UPDATE的锁行为完全一致,都基于执行计划决定锁粒度
INSERT 会触发隐式锁和插入意向锁(Insert Intention Lock)
新插入一行时,InnoDB 并不会直接加记录锁,而是先判断插入位置是否被其他事务用临键锁封锁。如果没有冲突,就完成插入;如果有,就等待对方释放间隙上的锁。这时等待方持有的是 **插入意向锁(Insert Intention Lock)**——一种特殊的间隙锁,表示“我想在这个间隙插一条记录”。
关键点在于:插入意向锁之间互不冲突(多个事务可以同时申请同一间隙的插入意向锁),但会与临键锁/间隙锁冲突。这也是为什么两个事务同时 INSERT INTO t VALUES (5) 到同一个空缺位置时不会死锁,但如果一个事务持有 WHERE id > 3 AND id 的临键锁,另一个事务插 <code>id = 5 就会被阻塞。
- 插入前会检查插入点所在间隙是否被其他事务的临键锁覆盖
- 插入意向锁本身不阻塞其他插入意向锁,只阻塞临键锁/间隙锁
- 自增主键插入会额外持有
auto-inc lock(表级锁),影响并发 INSERT 性能 - 唯一键冲突时,INSERT 会先加记录锁去查冲突行,再决定报错 or 更新(ON DUPLICATE KEY)
如何快速确认某条 SQL 实际加了什么锁?
别猜,用 INFORMATION_SCHEMA.INNODB_TRX + INNODB_LOCKS(MySQL 5.6/5.7)或 performance_schema.data_locks(8.0+)直接查。最实用的是结合 SHOW ENGINE INNODB STATUS\G,看其中的 TRANSACTIONS 和 LATEST DETECTED DEADLOCK 部分。
更轻量的方式是:开启事务,执行目标 SQL,然后立刻查:
SELECT * FROM performance_schema.data_locks\G
输出里重点关注 LOCK_TRX_ID、LOCK_MODE(如 RECORD、REC_GAP、NEXT_KEY)、LOCK_DATA(具体锁住的索引值)。
- MySQL 8.0 必须开启
performance_schema且设置innodb_monitor_enable = "all"才能捕获完整锁信息 -
LOCK_MODE = X, REC_NOT_GAP表示独占记录锁;X, GAP是间隙锁;X, NEXT_KEY是临键锁 - 如果看到大量
WAITING状态的锁,说明有阻塞,配合data_lock_waits查谁在等谁 - 不要依赖 explain 判断锁类型——explain 只显示执行计划,不反映锁行为
实际并发场景中,锁的复杂性往往藏在“看似简单”的条件里:比如一个 ORDER BY created_at LIMIT 1 查询,如果 created_at 没索引,可能触发全表扫描+全表临键锁;又比如批量 UPDATE 用子查询,子查询结果集越大,锁住的索引范围越广。这些细节不查 data_locks 很难定位。










