mysql更新不存在的记录会加间隙锁,是rr隔离级别下防止幻读的刚性设计;只要where条件走索引且能界定范围(如唯一索引等值不命中、范围查询、非唯一索引等值匹配),innodb必加gap lock或next-key lock。

MySQL 更新不存在的记录也会加锁,不是 bug,是 REPEATABLE READ 隔离级别下防止幻读的强制行为;只要 WHERE 条件走索引、能界定扫描范围,哪怕 EXPLAIN 显示 rows=0,InnoDB 仍会加间隙锁(Gap Lock)或临键锁(Next-Key Lock)。
UPDATE 没命中行却加了 Gap Lock 怎么验证
不能只看 ROW_COUNT() 或 affected_rows 为 0 就认为“没锁”。真实锁状态得靠并发行为和系统表交叉验证:
- 在事务 A 中执行
UPDATE t SET x = 1 WHERE id = 500(假设id是主键,且表中无id = 500),不提交 - 在事务 B 中执行
INSERT INTO t (id, x) VALUES (500, 'y')→ 会被阻塞,说明间隙已被锁住 - 查
performance_schema.data_locks:过滤OBJECT_NAME = 't',若看到LOCK_MODE = 'X,GAP'或LOCK_MODE = 'X,REC_NOT_GAP'且LOCK_DATA显示边界值(如499、501),即确认是间隙锁 -
SHOW ENGINE INNODB STATUS\G中关注lock_mode X locks gap before rec这类提示,它不会出现在INFORMATION_SCHEMA.INNODB_TRX里
哪些 UPDATE 场景一定会触发间隙锁(即使没数据)
触发间隙锁的关键不是“有没有匹配行”,而是“优化器是否按索引范围扫描 + RR 隔离级别”:
- 唯一索引(含主键)上的等值查询但值不存在:如
UPDATE users SET status = 1 WHERE user_id = 9999,user_id有唯一索引但无此值 → 加 Gap Lock 锁住前后两个实际存在的user_id之间的空隙 - 普通索引或联合索引上的等值查询:如
UPDATE orders SET paid = 1 WHERE status = 'pending',即使只有一条匹配,也会锁住该status值对应索引位置前后的间隙 - 任何索引上的范围条件:如
UPDATE logs SET archived = 1 WHERE created_at > '2026-09-01',哪怕当前没有一条满足该时间的数据,也会锁住索引中该时间点之后的所有间隙 -
LIKE前缀匹配(有索引支持):如WHERE name LIKE 'abc%',本质是范围扫描,同样触发 Gap Lock
怎么避免“没更新却锁住别人插入”的问题
核心思路不是禁用间隙锁(那会破坏 RR 语义),而是让 MySQL 只锁“必须锁的记录”,绕过间隙锁的触发条件:
- 改用
READ COMMITTED隔离级别:执行SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,此时间隙锁被禁用,没命中的UPDATE真的不加锁(但需注意 binlog_format 必须为ROW才安全) - 确保 WHERE 条件命中真实数据:先用
SELECT id FROM t WHERE ... LIMIT 1查是否存在,仅当存在时才执行UPDATE;避免“先 UPDATE 再判断影响行数”这种伪原子操作 - 用唯一约束替代应用层检查:比如插入前查是否存在,不如直接
INSERT ... ON DUPLICATE KEY UPDATE,由数据库保证幂等性,也规避了间隙锁竞争 - 避免全表扫描:如果
WHERE没走索引(如隐式类型转换、函数包裹字段),InnoDB 会退化为锁所有扫描行 + 意向锁,这比间隙锁更危险——加索引、修正类型、去掉YEAR(created_at)类表达式才是治本
最易被忽略的一点:间隙锁本身不可见、不占内存、不显示在事务列表里,但它会让 INSERT/UPDATE 在毫无征兆的情况下卡住。排查时别只盯着 INNODB_TRX,一定要结合 data_locks 和并发行为交叉印证。











