无索引的update会全表扫描并逐行加next-key锁,导致锁行数接近总行数,并非锁升级而是锁数量爆炸;需通过explain关注type、rows、extra字段识别隐式失效,并建合适联合索引验证trx_rows_locked下降。

无索引的 UPDATE 不会“主动升级为表锁”,而是被迫全表扫描并逐行加行锁,最终效果等同于锁全表——这是 InnoDB 行锁机制的必然结果,不是配置或版本问题。
为什么没索引就等于锁全表
InnoDB 的行锁加在索引记录上,不是加在“数据行”上。当 WHERE city = 'Beijing' 这类条件没有对应索引时,优化器只能走 type = ALL 全表扫描,引擎层会对每一条被 Server 层送过来判断的聚簇索引记录都加 X 锁(记录锁)+ 间隙锁(Gap Lock)。哪怕最终只更新 1 行,InnoDB 也得先扫完整张表才能确认匹配项,所有被扫描的主键都被锁住。
- 可重复读(RR)隔离级别下,
UPDATE自动使用 next-key 锁,连带锁定值之间的间隙,阻塞范围进一步扩大 -
SHOW ENGINE INNODB STATUS\G中若看到trx_rows_locked接近表总行数,就是典型信号 - 不是“锁升级”,是“锁数量爆炸”:100 万行的表,扫完就持有了 100 万个行锁,内存开销和等待链都远超表锁
EXPLAIN 看不出问题?这些细节常被忽略
EXPLAIN 是第一道验证工具,但光看 key 是否为 NULL 不够,还要盯紧三个字段:
-
type是ALL或index→ 实际在全表或全索引扫描,已失效 -
rows接近表总行数(比如表有 80 万行,rows=795231)→ 扫描范围没收缩 -
Extra出现Using where; Using index condition是好的;但若只有Using where,且key为空,说明索引根本没参与过滤
常见“假命中”场景:WHERE user_id = '123'(user_id 是 BIGINT),隐式类型转换让索引失效;WHERE DATE(created_at) = '2026-01-01',函数包裹直接跳过索引。
建了索引还是锁很多?顺序和分布才是关键
单列索引不等于万能解药。如果 status 字段区分度极低(95% 值都是 'pending'),即使有索引,UPDATE ... WHERE status = 'pending' 仍会锁住几十万行。
- 多条件查询必须用联合索引,且顺序严格按“等值在前、范围在后”:对
WHERE status = ? AND created_at > ?,要建idx_status_created(status, created_at),反过来无效 -
FORCE INDEX不能绕过底层逻辑:对无索引字段强制索引会报错或被忽略;对存在但不匹配的索引(如用(created_at)查status = ? AND created_at > ?),可能退化为索引全扫描,锁更多无关行 - READ COMMITTED 隔离级别可禁用间隙锁,减少锁范围,但需评估业务能否容忍幻读
怎么确认修复真生效了
别只测执行时间变快,要验证锁行为是否收敛:
- 在测试库开事务执行该
UPDATE(不提交),立刻查:SELECT trx_id, trx_rows_locked, trx_state FROM information_schema.INNODB_TRX,对比加索引前后trx_rows_locked是否从百万级降到几百/几千 - 两个会话并发执行相同条件的
UPDATE:加索引前会互相阻塞;加索引后,只要更新不同行,应能并行完成 - 注意隐含依赖:加主键或唯一索引后,
INSERT ... ON DUPLICATE KEY UPDATE行为会变,必须同步检查业务代码
最容易被跳过的一步:分批更新脚本上线前,必须在备份库完整跑通,核对最终更新总数——因为 WHERE 条件里混了状态字段时,中间若有新插入或并发修改,MAX(id) 游标法也可能漏更。











