MySQL在REPEATABLE READ下BETWEEN触发next-key lock,锁定(9,25]区间,故id=21被阻塞;改用IN+先查后更新可避免间隙锁,但需控制数量≤1000并防范数据变更风险。

WHERE id BETWEEN 10 AND 20 为什么锁住 21?
因为 MySQL 在 REPEATABLE READ 隔离级别下,BETWEEN 触发的是 next-key lock(临键锁),它锁定的是「索引记录 + 前一个记录之间的左开右闭区间」。假设主键值为 9、10、15、20、25,执行 UPDATE t SET x=1 WHERE id BETWEEN 10 AND 20,InnoDB 实际加锁区间是 (9, 25]——21 就落在这个间隙里,插入会被阻塞。
这不是误判,而是设计使然:间隙锁本质是防止幻读,它锁的是「空位」,不是数据本身。
- 即使
id = 21当前不存在,也会被拦住 -
LOCK IN SHARE MODE同样会加间隙锁,改用它没用 - 只要走范围扫描、且隔离级别是 RR,就逃不开间隙锁
用 IN 替代 BETWEEN 是最直接的绕过方式
前提是能先查出确定存在的主键值,再用 IN 批量更新。此时每条 id = ? 都是唯一等值查找,只加 record lock,不加间隙锁。
示例流程:
SELECT id FROM orders WHERE status = 'pending' AND created_at BETWEEN '2024-01-01' AND '2024-01-31' ORDER BY id;
拿到结果如 (101, 102, 105, 108) 后,执行:
UPDATE orders SET status = 'processing' WHERE id IN (101, 102, 105, 108);
- 必须确保
IN列表 ≤ 1000 项,否则优化器可能放弃索引,退化为全表扫描 - 两次查询之间需考虑数据变更风险,建议加应用层幂等或重试逻辑
- 不能直接用
FORCE INDEX救BETWEEN—— 没对应索引时,语法通过但执行计划仍是type: ALL
如果必须用 BETWEEN,怎么收窄间隙范围?
核心是让 InnoDB 锁的区间尽可能小,避免 (x, +∞) 这种无限后缀。
- 显式加上界:把
WHERE id > 1000改成WHERE id > 1000 AND id ,锁区间从 <code>(1000, +∞)缩为(1000, 1500] - 联合索引压缩范围:例如建
INDEX idx_status_ctime_id (status, created_at, id),再写WHERE status = 'pending' AND created_at BETWEEN '2024-01-01' AND '2024-01-31',InnoDB 只锁该状态+该时间段内的索引片段,而非整个created_at轴 - 避免索引失效:别在字段上套函数,比如
DATE(create_time) = '2024-01-01'会让索引失效,改用create_time >= '2024-01-01' AND create_time
分批操作时,别用 BETWEEN x AND y 做分页
用 WHERE id BETWEEN 10000 AND 20000 分批删除或更新,极易因 id 不连续、并发插入导致间隙重叠,引发死锁。更稳的做法是:
DELETE FROM orders WHERE id > 10000 ORDER BY id LIMIT 100;
-
id > last_id天然保证单向推进,无重叠、无回溯 -
ORDER BY id LIMIT N强制走主键索引,避免优化器选错路径 - 每批执行后必须
COMMIT,释放锁并切断事务上下文 - 真实生产中建议加应用层休眠(如
sleep(0.1)),缓解锁竞争压力
真正难防的不是间隙锁本身,而是它和业务逻辑耦合后的隐式依赖:比如你按 status 批量更新,另一路逻辑却在疯狂往同一 status 组里插入新记录——这时候哪怕索引再好,间隙锁也会成为天然瓶颈。


















