GapLock 在 RR 隔离级别下自动触发,非可选功能;只要走索引且条件非唯一(如 status = 'pending'),InnoDB 就加间隙锁防幻读,导致并发写入下降;RC 级别禁用 GapLock,仅保留记录锁和插入意向锁,可有效缓解阻塞。

GapLock 在 RR 隔离级别下自动触发,不是可选功能
MySQL 的 REPEATABLE READ(RR)隔离级别下,InnoDB 默认启用间隙锁(Gap Lock),只要语句走索引(即使是二级索引)、且查询条件是非唯一值(如 WHERE status = 'pending'),就会对索引区间加锁,而不仅锁住匹配行。这不是 bug,是 RR 级别防止幻读的机制设计——但代价是并发写入能力明显下降。
常见错误现象:INSERT 或 UPDATE ... WHERE 被莫名阻塞,SHOW ENGINE INNODB STATUS 里看到 lock_mode X locks gap before rec insert intention waiting,说明正在等一个 GapLock 释放。
- 只有显式使用
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE才会加记录锁+间隙锁;但普通UPDATE/DELETE在 RR 下也隐式加 GapLock - 唯一索引等值查询(如
WHERE id = 123)不加 GapLock,只加记录锁;范围查询(WHERE id > 100)或非唯一索引查询一定会加 -
READ COMMITTED(RC)级别下 GapLock 被禁用(仅保留“插入意向锁”和“记录锁”),幻读可能产生,但并发写入冲突大幅减少
降低 GapLock 影响最直接的方式:切到 READ COMMITTED
如果业务能接受“同一事务中两次 SELECT 可能读到新插入的行”(即允许幻读),把隔离级别调成 READ COMMITTED 是见效最快的方案。它让 InnoDB 不再为范围条件预留间隙,所有锁退化为精确的记录锁 + 插入意向锁。
操作很简单,在连接初始化时执行:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
或者在 JDBC URL 中加上 sessionVariables=transaction_isolation='READ-COMMITTED'。
- RC 下
UPDATE t SET x=1 WHERE status='pending'只锁住实际命中的行,不会锁住status='pending'对应的整个索引间隙 - RC 不影响 MVCC 读性能,快照读逻辑不变;只是当前读(
UPDATE/DELETE)不再加 GapLock - 注意:全局修改需改
my.cnf中的transaction_isolation,但建议按业务连接粒度控制,避免影响强一致性场景
不改隔离级别时,靠索引设计缩小 GapLock 范围
GapLock 锁的是索引树上的“间隙”,所以索引越精准、范围越小,被锁住的间隙就越少。比如给 status 单列建索引,WHERE status = 'pending' 会锁住整个 status='pending' 的索引段;但如果加上时间条件并建立联合索引,就能把锁压缩到更小物理区间。
示例:原语句 UPDATE orders SET state='done' WHERE status='pending',表上有 INDEX(status)。优化后建联合索引:
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);
再改写查询为 WHERE status='pending' AND created_at 。
- 联合索引让查询从“扫全量 pending”变成“扫最近一小时 pending”,GapLock 范围大幅收窄
- 避免在
WHERE中使用函数(如DATE(created_at))或隐式类型转换,否则索引失效,退化为全表扫描+全间隙锁定 - 如果业务允许,把
status字段拆成带时间分区的子表(如orders_pending_202405),也能天然隔离间隙范围
用 SELECT ... FOR UPDATE NOWAIT 主动暴露锁冲突
GapLock 导致的阻塞常静默等待,直到超时(默认 50 秒),很难定位瓶颈点。用 NOWAIT 强制立即报错,能把问题提前暴露到应用层,便于监控和降级。
例如在扣减库存前做预检:
SELECT stock FROM inventory WHERE sku = 'ABC' FOR UPDATE NOWAIT;
若返回 ERROR 1205 (40001): Deadlock found when trying to get lock 或 ERROR 3572 (HY000): Statement aborted because lock(s) could not be acquired immediately,说明有 GapLock 冲突,可快速熔断或重试。
-
NOWAIT仅在 MySQL 8.0.1 以上支持;低版本可用SET innodb_lock_wait_timeout = 1;模拟,但不够精准 - 不要滥用:高频
NOWAIT查询本身也会增加锁竞争,适合关键路径(如下单、支付)的强校验环节 - 配合
INFORMATION_SCHEMA.INNODB_TRX和INNODB_LOCK_WAITS表,能查出谁持有了哪个间隙锁
GapLock 的“保护性”和“阻碍性”是一体两面,真正难的不是关掉它,而是判断哪条 SQL 真正需要它——比如资金流水类业务必须防幻读,但内容发布状态变更往往不需要。盲目调低隔离级别或硬加索引,可能掩盖了本该由应用层处理的并发逻辑问题。











