select for update 锁住“不存在的记录”实为加间隙锁,如id=3时锁(1,5)区间以防止幻读;其锁类型取决于索引类型与查询条件是否命中唯一索引,需通过explain验证执行计划确保精准锁定。

SELECT FOR UPDATE 为什么锁住“不存在的记录”
它根本没锁“记录”,而是锁住了索引中该值**应该插入的位置所对应的间隙**。比如主键 id 当前有 1 和 5,执行 SELECT * FROM t WHERE id = 3 FOR UPDATE,InnoDB 实际加的是间隙锁 (1, 5)——这个区间里没有数据,但任何试图插入 id = 3 或 id = 4 的语句都会被阻塞。
这是 REPEATABLE READ 隔离级别下防止幻读的默认行为,不是 bug,也不是配置错误。
- 唯一索引 + 等值查询(
=)且记录存在 → 只加记录锁(Record Lock) - 唯一索引 + 等值查询但记录不存在 → 加间隙锁(
Gap Lock),范围是前后两个实际索引值之间 - 非唯一索引或范围查询(
>、、<code>BETWEEN)→ 默认加Next-Key Lock(记录锁 + 间隙锁)
哪些查询会意外触发间隙锁
最容易踩坑的是那些「看起来只查一行,实际走不了唯一索引」的语句。关键不在于你写了什么条件,而在于 MySQL 实际怎么执行它。
用 EXPLAIN 验证:key 为 NULL 或 rows 远大于预期,就说明锁范围不可控。
-
SELECT * FROM order WHERE user_id = 123 AND status = 'unpaid' FOR UPDATE:如果status没索引,优化器可能走user_id索引后回表过滤,查不到行时仍会锁整个user_id对应的间隙 -
SELECT * FROM product WHERE name LIKE 'iphone%':前缀模糊匹配,属于范围扫描,必然触发间隙锁 -
SELECT * FROM t WHERE create_time > '2024-01-01' FOR UPDATE:时间字段无索引?直接全表扫描 → 锁整张表(在 RR 下可能退化为表级锁)
如何让 SELECT FOR UPDATE 真正只锁一行
锁行为完全由 WHERE 条件是否命中唯一索引决定。哪怕业务上 order_no 绝对唯一,只要没建 UNIQUE KEY,InnoDB 就无法精确锁定单行或单间隙。
- 必须确保查询字段有唯一索引(如
UNIQUE KEY idx_order_no (order_no)) - 避免在
WHERE中使用函数、隐式类型转换、OR、LIKE '%xxx'等导致索引失效的写法 - 用
EXPLAIN验证执行计划:type应为const或ref,key显示命中索引,rows = 1 - 如果只是想“查不存在则插入”,优先用
INSERT ... ON DUPLICATE KEY UPDATE,靠唯一约束本身控制冲突,完全绕过SELECT FOR UPDATE
RR 和 RC 隔离级别对间隙锁的影响
REPEATABLE READ 是 MySQL 默认隔离级别,间隙锁在此级别下默认启用;READ COMMITTED 下默认不加间隙锁,但要配合 binlog_format = ROW 才安全,且会牺牲可重复读语义。
切隔离级别不是解药——它只是把幻读风险从加锁转移到应用层校验。很多团队改完 RC 后发现业务逻辑出错,就是因为原本靠间隙锁挡住的并发插入,现在漏进来了。
真正需要关注的是:你的 SELECT FOR UPDATE 是否真的需要锁住“不存在”的位置?如果不需要,那就得从索引设计和查询写法入手,而不是调低隔离级别。











