非主键索引UPDATE会锁二级索引项和对应聚簇索引主键行两处,因加锁非原子且RR级别下加间隙锁,易引发死锁;无索引时则隐式锁全表。

因为非主键索引更新必然触发回表,InnoDB 会分两步加锁:先锁二级索引项,再锁对应聚簇索引主键行——这两步不原子,且在 RR 隔离级别下还额外加间隙锁,锁范围远超语句表面意图。
非主键索引 UPDATE 为什么锁两处
MySQL 的行锁本质是锁索引,不是锁记录。当 WHERE 条件命中非唯一二级索引(如 KEY idx_status (status))时,InnoDB 必须:
- 先通过该索引定位数据,对匹配的索引项加 record lock 或 next-key lock
- 再根据索引叶子节点里的主键值,回表到聚簇索引上查找并锁定对应行,加 X 锁
这两把锁不是原子获取的,中间存在时间窗口。比如事务 A 已持 idx_status 锁、正等主键锁;事务 B 已持主键锁、正等 idx_status 锁——死锁立即发生。
即使你只更新一行,只要用的是非唯一索引,就逃不开这个双锁路径。
RR 隔离级别下间隙锁会让锁范围爆炸
在默认的 REPEATABLE READ 下,非唯一索引的等值条件(如 WHERE status = 1)不仅锁匹配项,还会加 gap lock 或 next-key lock 防幻读。
- 假设
idx_status中status = 1的记录分布在主键id = 100和id = 200之间,InnoDB 可能锁住区间(100, 200) - 此时另一个事务想插入
status = 1, id = 150就会被阻塞 - 若该事务同时也在等某条主键行的锁,环形等待形成,
Deadlock found when trying to get lock就来了
READ COMMITTED 能关掉间隙锁,但解决不了回表带来的双索引加锁问题。
复合索引最左前缀非唯一也会触发同样逻辑
只要查询只用到复合索引的非唯一前缀,就和单列非唯一索引一样危险:
- 索引
KEY idx_u_i_s (user_id, item_id, status),语句UPDATE ... WHERE user_id = 123会走该索引 - 但
user_id不唯一 → 触发回表 + 间隙锁 - 如果
user_id = 123对应几百条记录,实际锁住的可能是整个user_id = 123对应的主键区间,外加前后间隙
用 EXPLAIN FORMAT=tree 确认是否走了索引;用 SELECT * FROM performance_schema.data_locks 查具体锁类型和范围,别靠猜。
没索引的 UPDATE 实际等于隐式锁表
当 WHERE 字段完全没索引,Server 层只能全表扫描,引擎层就不得不对每行聚簇索引记录加 next-key lock:
-
Rows_examined等于表总行数 -
trx_rows_locked接近表总行数(查INNODB_TRX) - 效果等同于锁表:其他事务的
SELECT FOR UPDATE、UPDATE全部卡住 -
sql_safe_updates=1不拦截这种场景——它只检查有没有WHERE或LIMIT,不验证索引是否真被用上
真正有效的预防手段只有三个:上线前强制 SQL 审核看 EXPLAIN 的 type 和 key;大表非主键字段更新必须建索引;高频组合条件宁可冗余建联合索引。
最容易被忽略的一点:死锁日志里看到的“锁冲突”,往往不是代码写错了,而是索引设计没对齐访问模式——比如批量更新按 status 和 created_at,却只给 status 单独建了索引。


















