根本原因是innodb行锁加在索引记录上,无索引时被迫全表扫描聚簇索引,并对每行加x锁+间隙锁,效果等同锁表。

等值更新(UPDATE ... WHERE col = ?)在没有索引时会触发全表锁定,根本原因不是 MySQL “想锁表”,而是 InnoDB 只能对索引项加锁;没索引,就只能扫聚簇索引(即整张表),并对每行加 X 锁 + Gap Lock —— 效果等同于锁表。
为什么没索引就等于锁全表
InnoDB 的行锁不是加在“数据行”上,而是加在“索引记录”上。当 WHERE 字段没索引,优化器被迫走 type = ALL 全表扫描,引擎层就得逐条判断并加锁:每扫描到一条聚簇索引记录,就加一个记录锁(REC_NOT_GAP);在可重复读(RR)隔离级别下,还会顺带把相邻间隙也锁住(GAP)。哪怕只有一行匹配,所有被扫描的主键值及其间隙都会被锁死。
-
EXPLAIN SELECT * FROM users WHERE mark_id = 123中type是ALL、key是NULL,基本可断定没走索引 - 即使
mark_id上建了索引,若该字段重复率极高(比如只有 3 种取值),优化器可能主动放弃索引,结果一样 - 隐式类型转换也会让索引失效:如
mark_id是VARCHAR,但传入数字123,MySQL 会转成字符串再比对,导致无法使用索引
强制索引(FORCE INDEX)有用吗
多数情况下没用。因为 FORCE INDEX 只影响访问路径选择,不改变锁行为本质:如果强制的索引本身不适合该查询(比如是单列索引但 WHERE 条件含多个字段),或该索引区分度太低,InnoDB 仍可能退化为全扫描;更关键的是,FORCE INDEX 对全表扫描场景下的加锁范围毫无约束力 —— 扫多少行,就锁多少行+间隙。
-
UPDATE users FORCE INDEX (idx_mark_id) SET status = 'done' WHERE mark_id = 123成功的前提是idx_mark_id真的被优化器采纳,且rows显著小于表总行数 - 用
EXPLAIN FORMAT=JSON查used_key和key_length,才能确认是否真用了索引 - 真正有效的解法是建**覆盖查询模式的联合索引**,而非依赖强制提示
如何验证和规避这种“伪等值更新”锁表
所谓“伪等值”,是指语句写法像等值查询(=),但因函数、表达式或类型问题,实际执行时已变成范围扫描或全表扫描。这类语句最容易在上线后突然引发大面积阻塞。
- 禁止在
WHERE左侧使用函数:WHERE DATE(created_at) = '2026-05-01'→ 改为WHERE created_at >= '2026-05-01' AND created_at - 高并发等值更新,优先走主键:
UPDATE orders SET paid = 1 WHERE id IN (SELECT id FROM orders WHERE mark_id = 123 LIMIT 100),再分批执行 - 上线前必做三件事:
EXPLAIN看执行计划、SHOW INDEX确认索引存在且有效、SELECT * FROM information_schema.INNODB_TRX查长事务残留
最易被忽略的一点:锁表时间 ≠ SQL 执行时间,而是从 BEGIN 到 COMMIT 的整个事务窗口。哪怕 UPDATE 本身毫秒完成,只要事务里混了日志、HTTP 调用或慢查询,锁就会一直挂着,并持续扩大间隙锁范围。











