能直接缩小行锁范围的只有索引设计本身:没索引或索引用错会导致update/select for update锁一片甚至全表;where不走索引时锁整个聚簇索引,加临键锁等价于全表锁定。

能直接缩小行锁范围的,只有索引设计本身——没索引或索引用错,UPDATE 或 SELECT FOR UPDATE 就不是“锁一行”,而是“锁一片”甚至“锁全表”。
WHERE 条件没走索引,锁的是整个聚簇索引
MySQL 的行锁不是按物理行加的,而是按索引记录加的。WHERE 不走索引时,InnoDB 只能扫描聚簇索引(也就是整张表),对每条扫描到的记录加临键锁(next-key lock)。这等价于锁住所有行,还附带间隙锁,其他事务几乎无法并发修改任何数据。
-
EXPLAIN看到type是ALL或index,基本就确认是全表/全索引扫描 - 常见诱因:
WHERE status = 0但status是VARCHAR类型 → 隐式转换让索引失效 -
WHERE YEAR(created_at) = 2024这类函数操作,索引完全无法命中 - 联合索引
(a, b, c),却查WHERE b = 2 AND c = 3→ 不满足最左前缀,索引失效
非唯一索引下等值查询,锁的是临键区间而非单行
同样是 WHERE id = 123,如果 id 是主键或 UNIQUE 索引,InnoDB 只加一条记录锁(record lock);但如果 id 是普通二级索引(哪怕值唯一但没定义为 UNIQUE),就会加临键锁,锁住该值前后的间隙——这意味着可能锁住几十甚至上百行。
- 验证方式:
SHOW ENGINE INNODB STATUS\G查看RECORD LOCKS段,注意supremum pseudo-record和infimum pseudo-record出现频率 - 解决办法:把高频等值查询字段显式定义为
UNIQUE,或直接走主键更新 - 别依赖“业务上唯一”:MySQL 不认业务逻辑,只认索引定义
用覆盖索引避免回表,锁就只落在二级索引上
SELECT ... FOR UPDATE 如果能走覆盖索引(EXPLAIN 的 Extra 显示 Using index),就不需要访问聚簇索引,也就不会对主键记录加锁。锁只落在二级索引页里对应的一条索引项上,粒度最小。
- 建法示例:想按
user_id锁余额并更新,建INDEX idx_uid_balance (user_id, balance) - 写法必须匹配:
SELECT balance FROM users WHERE user_id = 1001 FOR UPDATE—— 所有字段都在索引中 -
SELECT *或SELECT name(name不在索引里)→ 必须回表 → 锁落到主键记录 + 间隙 - 注意:
ORDER BY和LIMIT是否缩小锁范围,取决于是否仍能走索引;如果排序字段不在索引中,照样触发全扫描加锁
复合索引顺序错了,等于没建索引
等值查询字段必须放前面,范围查询字段放后面。顺序反了,不仅查不到,加锁还会扩大——比如索引是 (created_at, user_id),但总用 WHERE user_id = 100 查询,优化器根本没法跳过 created_at 去定位,结果就是扫描所有 created_at 值,锁住所有匹配 user_id 的行。
- 正确顺序示例:
WHERE user_id = 100 AND created_at > '2024-01-01'→ 索引应为(user_id, created_at) - 高频过滤字段靠前,哪怕它区分度不如时间字段——锁范围由第一个没命中的字段决定
- IN 子句参数过多(如
WHERE id IN (1,2,...,2000))可能让优化器放弃使用索引,退化为全索引扫描 → 锁整个索引范围
真正容易被忽略的不是“要不要建索引”,而是“建了之后 MySQL 实际有没有用、用得对不对”。EXPLAIN 必须每次上线前跑一遍,SHOW ENGINE INNODB STATUS 要定期看锁的分布,否则索引只是摆设,锁还是照常扩散。











