Gap Lock不是bug而是InnoDB在RR级别下防幻读的机制,其性能问题源于无索引或非唯一索引导致间隙锁范围失控;唯一索引+等值查询可彻底规避Gap Lock,仅加Record Lock。

Gap Lock 不是 bug,而是 InnoDB 在 REPEATABLE READ 隔离级别下防止幻读的默认机制;它真正伤性能的地方,是让“没命中数据”的查询也锁住一大片索引间隙——而索引设计不当,会让这个间隙变得极大、不可控。
为什么无索引或非唯一索引会放大 Gap Lock 范围
Gap Lock 只作用于索引结构。当 WHERE 条件无法走索引(如 name = 'alice' 但 name 没索引),InnoDB 退化为全表扫描,并对整个聚簇索引加 Next-Key Lock,等效于锁整张表。即使有索引,若不是唯一索引且条件为范围查询(如 status IN (1,2,3)),也会锁定多个间隙段。
- EXPLAIN 输出中
type是ALL或index,基本意味着 Gap Lock 范围失控 - 复合索引未覆盖最左前缀(如索引是
(user_id, created_at),但查询只用created_at > '2024-01-01'),InnoDB 可能放弃该索引,触发全扫描加锁 - 主键或唯一索引上的等值查询(
id = 123或order_no = 'ORD-2024-xxx')只会加 Record Lock,不触发 Gap Lock
用唯一索引 + 等值条件彻底绕过 Gap Lock
只要查询能精确命中唯一索引(主键或 UNIQUE 约束字段),InnoDB 就只加 Record Lock,不扩展到间隙。这是规避 Gap Lock 最干净的方式。
- 把业务关键查询字段设为
UNIQUE:比如订单号、用户手机号、设备 ID,哪怕只是逻辑唯一,也建议加UNIQUE约束并建索引 - 避免在唯一索引字段上做范围查询:
WHERE order_no LIKE 'ORD-2024%'会退化为范围扫描,仍可能触发 Gap Lock - 确认索引生效:对
SELECT * FROM orders WHERE order_no = ? FOR UPDATE执行EXPLAIN,确保key列显示索引名,rows≈ 1
READ COMMITTED 隔离级别下 Gap Lock 被禁用,但要小心副作用
MySQL 在 READ COMMITTED 下关闭 Gap Lock 和 Next-Key Lock,只保留 Record Lock。这能立刻缓解 INSERT 阻塞问题,但代价是允许幻读。
- 临时切换可用
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,适合报表类、统计类或“读已提交即可”的事务 - 不能全局改成
READ COMMITTED:某些强一致性场景(如资金转账两次查余额)依赖 RR 的可重复读语义 - 注意:即使在 RC 下,
UPDATE ... WHERE若没走索引,仍会锁全表——索引仍是前提
范围查询不可避免时,如何收窄 Gap Lock 影响
有些业务必须范围查询(如后台批量改状态:UPDATE orders SET status = 2 WHERE user_id = 123 AND created_at > '2024-06-01'),这时 Gap Lock 无法消除,只能控制其范围。
- 给范围字段加联合索引,且把等值字段放最左:
INDEX (user_id, created_at),这样 Gap Lock 仅限于该user_id对应的created_at区间,而非全表 - 避免在范围条件里混用函数:
WHERE DATE(created_at) = '2024-06-01'会导致索引失效,Gap Lock 扩散 - 分页更新时慎用
LIMIT:UPDATE ... WHERE status = 0 ORDER BY id LIMIT 100在 RR 下可能锁住后续未命中的间隙,建议改用WHERE id > ? AND status = 0 LIMIT 100并记录上次id
Gap Lock 的“黑洞”本质是索引与隔离级别的耦合效应;真正可控的解法从来不在锁本身,而在让每一行查询都落到唯一、精准、可预测的索引路径上——否则调小 innodb_lock_wait_timeout 或重试,只是把阻塞变成失败,问题还在原地。



















