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很难定位。
