幻读是事务中两次查询同一范围数据时第二次多出新插入的行;临键锁通过结合行锁与间隙锁锁定记录及区间,防止其他事务在间隙中插入数据,仅在可重复读隔离级别且索引范围查询时生效。

什么是幻读,为什么临键锁能解决它
幻读不是“看到幻觉”,而是事务中两次 SELECT 同一范围数据,第二次多出新插入的行——这些行在第一次查询时并不存在,但又被当前事务“意外”读到。可重复读(RR)隔离级别下,普通行锁挡不住其他事务在间隙里插数据,NEXT-KEY LOCK 就是把行锁 + 间隙锁合起来用:既锁住现有记录,也锁住记录前的空档(比如 (10, 20) 这个区间),让别人插不进中间。
注意:这机制只在 RR 隔离级别 + 索引条件查询时生效;全表扫描或无索引字段上查,InnoDB 可能退化为表级锁或锁不住间隙。
怎么确认自己真的用了 NEXT-KEY LOCK
光设成 RR 隔离级别不够,必须满足两个前提:查询条件命中索引(最好是唯一/普通二级索引),且语句是范围查询(如 WHERE id > 5、WHERE name BETWEEN 'a' AND 'c')。否则 InnoDB 可能只加行锁或间隙锁,甚至不锁。
验证方法:
- 执行
SELECT * FROM performance_schema.data_locks;查看当前锁类型,找LOCK_MODE值为RECORD且带GAP或NEXT-KEY的记录 - 用
SHOW ENGINE INNODB STATUS\G看TRANSACTIONS段,留意lock_mode X locks rec but not gap表示没用上临键锁 - 在事务中执行
SELECT ... FOR UPDATE范围查询后,另一个事务尝试INSERT到该范围,若被阻塞,说明临键锁生效
哪些情况会让 NEXT-KEY LOCK 失效或降级
临键锁很“娇气”,稍不注意就退化:
- 查询字段没走索引(例如
WHERE status = 'pending'但status无索引)→ 只能锁表或不锁间隙 - 使用了
SELECT ... LOCK IN SHARE MODE且查询是等值唯一索引 → 只加记录锁,不锁间隙 - 主键等值查询(
WHERE id = 10)→ 只锁单条记录,不锁间隙(因为没“范围”) - MySQL 8.0+ 开启了
innodb_locks_unsafe_for_binlog = ON(已废弃但旧配置可能残留)→ 会禁用间隙锁
最常见踩坑点:开发时本地用小数据量测试,没幻读;上线后并发高、数据多,间隙变大,插数据绕过锁,幻读就冒出来了。
替代方案与实际取舍
如果业务逻辑实在绕不开幻读,又不想强依赖临键锁(比如写法复杂、死锁风险高),可以考虑:
- 改用
SELECT ... FOR UPDATE+ 应用层重试:先查再插,冲突时捕获Deadlock found when trying to get lock或Lock wait timeout exceeded错误,回退重试 - 在应用侧加分布式锁(如 Redis),控制同一业务键的并发写入,但增加外部依赖
- 接受幻读,改用应用逻辑兜底(比如订单号生成加唯一约束,靠数据库报错拦截重复)
临键锁不是银弹——它靠索引结构和查询方式“碰巧”生效,一旦 WHERE 条件松动、索引失效、或者升级 MySQL 版本调整了锁策略,行为就可能变。线上排查时,永远先看 EXPLAIN 和 data_locks,别信“设了 RR 就万事大吉”。











