mysql批量update锁住大片id,因where未精准命中主键或唯一索引,innodb退化为范围扫描并升级为next-key lock(锁记录+间隙);须用主键范围分批(如where id between ? and ?),每批100–500行,取max(id)续查并及时commit。

UPDATE多行时为什么突然锁住一大片ID
因为WHERE条件没精准落到主键或唯一索引上,InnoDB会退化为范围扫描,自动升级成next-key lock——既锁记录,也锁间隙。比如UPDATE orders SET status = 'done' WHERE created_at > '2024-01-01',哪怕created_at有索引,只要不是联合唯一索引,就可能锁住(2024-01-01, +∞)整个后缀区间,新订单插入直接卡死。
必须用主键范围过滤,不能只靠LIMIT
很多人误以为LIMIT 100就能控制锁范围,其实不行。MySQL仍可能全表扫描再截断,锁住所有扫描过的间隙。真正安全的方式是显式限定主键区间:
-
WHERE id BETWEEN ? AND ?是最稳妥的写法,确保执行计划type为range且key显示命中主键 - 起始值必须取上一批的
MAX(id),不能简单+100(否则漏数据或重复) - 步长建议 100–500 行:太大易触发锁升级;太小则事务开销压垮TPS
- 每批执行后必须
COMMIT,释放锁并切断事务上下文
非主键字段更新时怎么绕过间隙锁
如果业务硬要按email LIKE '%@gmail.com'这类非索引条件批量更新,InnoDB只能全表扫,等于事实锁表。可行解法只有两个:
- 先建临时索引:
CREATE INDEX idx_email_substring ON users(email);(DDL期间表可读不可写,但比锁表几小时好) - 改用两阶段操作:先
SELECT id FROM users WHERE email LIKE '%@gmail.com' ORDER BY id LIMIT 500拿到ID列表,再UPDATE users SET flag = 1 WHERE id IN (1,2,3,...)——注意IN列表别超 1000 项,否则优化器可能放弃索引 - 别信
FORCE INDEX能救场:没对应索引时,语法通过但执行计划仍是type: ALL
联合索引设计如何缩小间隙锁影响
如果必须用非主键字段做范围条件,联合索引是收窄锁范围的关键。例如想按状态+时间批量更新订单:
- 建索引:
CREATE INDEX idx_status_ctime_id ON orders(status, created_at, id); - 查询时必须按最左匹配:
WHERE status = 'pending' AND created_at BETWEEN '2024-01-01' AND '2024-01-31' - 这样InnoDB只锁该状态+该时间段内的索引片段,间隙范围大幅压缩,而不是锁整个
created_at轴 - 若业务允许,更彻底的做法是把范围条件拆成
IN:先查出ID列表,再单点更新——此时每个id = ?都只加record lock,零间隙锁
真正难防的不是间隙锁本身,而是它和业务逻辑耦合后的隐式依赖:比如你按status批量更新,但另一路逻辑正往同一状态组里疯狂INSERT新记录——这时候哪怕索引再好,间隙锁也会成为天然瓶颈。得从数据流设计上提前切分写入路径,而不是只在SQL里打补丁。











