是必然。索引失效时InnoDB退化为全表或全索引扫描,对所有扫描到的聚簇索引记录加Next-Key Lock或Gap Lock,导致锁范围急剧扩大;即使EXPLAIN显示走索引,若条件含IS NULL、非唯一二级索引无覆盖等,仍会触发间隙锁或全区间锁定。

索引失效直接导致锁范围扩大——不是“可能”,而是必然。InnoDB 在无法精确定位行时,只能对扫描路径上的所有聚簇索引记录加 Next-Key Lock 或 Gap Lock,哪怕你只想要改 1 行,也可能锁住几百行甚至整个索引区间。
为什么 EXPLAIN 显示走了索引,但锁还是爆炸?
关键误区:把 key 非 NULL、type 是 range 当成“锁很轻”。实际锁行为取决于三件事:
- 是否命中唯一索引(主键或
UNIQUE)→ 只锁单行 - 是否用非唯一二级索引 + 无覆盖 → 先锁索引项,再回表,间隙锁极易触发
- WHERE 条件是否含
IS NULL、!=、NOT IN、左模糊LIKE '%abc'→ 可能退化为全范围扫描加锁
典型例子:SELECT * FROM t_award WHERE pool_id = 123 AND identifier IS NULL,即使 pool_id 有索引,identifier IS NULL 无法走索引,InnoDB 往往对整个 pool_id = 123 的索引区间加间隙锁。
WHERE 中含 IS NULL 怎么避免锁扩大?
IS NULL 在 B+ 树里不占常规位置,优化器很难定位,基本等于放弃索引。修复核心是让 NULL 可索引化:
- 最简单:把字段改成
NOT NULL DEFAULT '',业务层用空字符串代替NULL - MySQL 8.0+ 可建函数索引:
CREATE INDEX idx_identifier_null ON t_award ((identifier IS NULL)) - 更稳妥(兼容性好):加生成列再索引:
ALTER TABLE t_award ADD COLUMN is_identifier_null TINYINT AS (CASE WHEN identifier IS NULL THEN 1 ELSE 0 END) STORED,然后建复合索引CREATE INDEX idx_is_null ON t_award (pool_id, is_identifier_null, status, is_redeemed),原查询改写为WHERE pool_id = ? AND is_identifier_null = 1 AND status = 0 AND is_redeemed = 0
如何验证锁是否真的被收敛?
不能只信 EXPLAIN。必须结合运行时证据:
- 第一步:用
SELECT ... FOR UPDATE模拟真实逻辑,再查INFORMATION_SCHEMA.INNODB_LOCK_WAITS和INNODB_LOCKS - 第二步:看
EXPLAIN FORMAT=TREE输出,确认有没有filesort节点,以及rows是否接近你预期的过滤后行数 - 第三步:开启
innodb_status_output_locks = ON,执行SHOW ENGINE INNODB STATUS\G,在TRANSACTIONS部分搜lock_mode和RECORD LOCKS行数
真正容易被忽略的是:即使索引生效,REPEATABLE READ 隔离级别下仍默认启用间隙锁;而 SELECT ... FOR UPDATE 不走索引时,锁范围和 UPDATE 一样大——很多人以为只有 DML 才锁得多,其实不然。


















