根本原因是innodb在rr隔离级别下默认启用间隙锁,它锁定索引中两个值之间的“空隙”而非数据本身;只要查询条件命中索引且未匹配记录,就加临键锁,如where phone='138xxx'会锁住(137xxx, 139xxx)区间。

SELECT FOR UPDATE 为什么锁住“不存在的行”
根本原因不是语句写错了,而是 InnoDB 在 REPEATABLE READ 隔离级别下默认启用间隙锁(Gap Lock)——它锁的不是数据本身,而是索引中两个值之间的“空隙”。哪怕 WHERE phone = '138xxx' 查不到任何记录,InnoDB 仍会锁定该值应插入的位置,比如 (137xxx, 139xxx) 这个区间。
RR 级别下间隙锁如何触发
只要查询条件命中索引(哪怕没匹配到记录),InnoDB 就会加临键锁(Next-Key Lock = 记录锁 + 间隙锁)。典型场景包括:
-
SELECT * FROM users WHERE name = 'Alice' FOR UPDATE,而表中没有Alice,但有Aaron和Bob→ 锁住(Aaron, Bob)间隙 -
SELECT * FROM orders WHERE order_no = 'NO20261014001' FOR UPDATE,该单号尚未生成 → 锁住相邻已存在单号之间的间隙 - 使用范围条件如
WHERE id BETWEEN 100 AND 200,即使当前只有 id=105 和 id=199 的记录,也会锁住(105, 199)及其前后间隙
为什么不能简单关掉间隙锁
禁用间隙锁最直接的方式是切隔离级别:SET TRANSACTION ISOLATION LEVEL READ COMMITTED。但这会带来明确代价:
- 丢失可重复读语义:同一事务内两次
SELECT可能返回不同结果(幻读) - 某些业务逻辑依赖 RR 的一致性保障,比如基于快照的对账、导出
- RC 下虽不加间隙锁,但
SELECT FOR UPDATE仍会加记录锁;若 WHERE 没走索引,依然可能锁全表
怎么验证某次查询到底锁了什么
别靠猜,查锁视图才是唯一可靠方式:
- 看当前活跃事务:
SELECT trx_id, trx_state, trx_query FROM INFORMATION_SCHEMA.INNODB_TRX - 查谁在等谁:
SELECT waiting_trx_id, blocking_trx_id, waiting_query, blocking_query FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS - MySQL 8.0+ 更推荐:
SELECT * FROM sys.innodb_lock_waits\G,字段更直观,还带sql_kill_blocking_connection
注意:blocking_query 显示 NULL 很常见——元凶事务可能早已执行完 SQL,正空挂着事务等待应用层下一步指令。











