mysql没走索引时等效锁全表,因innodb行锁加在索引记录上,无索引则全表扫描聚簇索引并逐行加临键锁,锁住所有行及间隙,导致并发写入阻塞。

WHERE条件没走索引就等于锁全表
MySQL的行锁实际加在索引记录上,不是数据行本身。一旦WHERE条件无法命中索引,InnoDB只能扫描聚簇索引(即整张表),对每条扫描到的记录加临键锁——效果等价于锁住所有行+所有间隙,其他事务基本无法并发写入。
常见错误现象:EXPLAIN显示type为ALL或index,key列为NULL;SHOW ENGINE INNODB STATUS\G里看到大量RECORD LOCKS且lock_mode含NEXT_KEY,甚至出现supremum pseudo-record。
- 隐式类型转换:如
user_id是INT,却写成WHERE user_id = '123',索引失效 - 函数操作字段:如
WHERE DATE(created_at) = '2026-09-01',改用created_at >= '2026-09-01' AND created_at - 联合索引顺序错:索引是
(a, b, c),但查询写WHERE b = 2 AND c = 3,不满足最左前缀,索引被跳过
非唯一索引下等值查询会锁间隙
同样是WHERE id = 123,如果id是主键或显式定义的UNIQUE索引,InnoDB只加REC_NOT_GAP记录锁;但如果id只是普通二级索引(哪怕业务上唯一),就会加临键锁,锁住该值前后的间隙——可能阻塞几十行甚至更多。
验证方式:SELECT * FROM performance_schema.data_locks WHERE OBJECT_NAME = 'your_table',重点看LOCK_MODE和LOCK_DATA。
- 高频等值查询字段必须建为
UNIQUE索引,不能依赖“业务上唯一” - 若无法改索引定义,优先改查询逻辑:用主键代替二级索引做
FOR UPDATE,例如先查SELECT id FROM t WHERE idx_col = ?,再SELECT ... FOR UPDATE WHERE id = ? - 避免用
SELECT * FOR UPDATE配合非唯一索引,极易引发意外阻塞
覆盖索引能避开聚簇索引上的行锁
只要SELECT字段和WHERE条件全部落在同一个二级索引中,InnoDB就不需要回表查聚簇索引,锁只加在二级索引页上——粒度更小、冲突更低、持有时间更短。
典型场景:秒杀校验库存时只读user_id和stock,建INDEX idx_uid_stock (user_id, stock),再写SELECT stock FROM orders WHERE user_id = 1001 FOR UPDATE。
-
EXPLAIN中Extra必须显示Using index才算真正生效 -
SELECT *永远无法覆盖,除非表只有两三个字段且全建在索引里 - 联合索引字段顺序要匹配:等值条件字段放前面,查询返回字段放后面(如
(user_id, status, updated_at)对应WHERE user_id = ? AND status = ?并SELECT status, updated_at)
READ COMMITTED + 覆盖索引可消除间隙锁
在READ COMMITTED隔离级别下,InnoDB默认不加间隙锁,只对命中的索引记录加记录锁。结合覆盖索引,就能实现「只锁必要索引项、不锁间隙、不回表」的最小锁组合。
注意:这不适用于需要防止幻读的场景(比如资金类事务),但对大多数状态校验、幂等判断类操作足够安全。
- 确认当前会话级别:
SELECT @@transaction_isolation,设为READ-COMMITTED需SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED - 范围查询(如
WHERE created_at > ?)仍可能锁多个索引项,但不会像REPEATABLE-READ那样锁间隙 - 别为了省锁盲目降级隔离级别——先看业务是否真能容忍幻读
最常被忽略的一点:索引设计不是建完就完事,得让优化器真用上。每次上线新索引,必须用EXPLAIN跑一遍真实查询语句,看key列是不是你预期的那个索引,rows是不是明显下降,Extra有没有Using index。否则,锁范围还是和没建一样宽。











