UPDATE用到非唯一辅助索引时,InnoDB必须先锁二级索引项再回表锁聚簇索引主键行,两步非原子导致死锁风险;无索引则全表扫描并逐行加X锁,锁范围爆炸。

UPDATE用到辅助索引时,InnoDB必须回表才能完成更新
因为InnoDB的辅助索引(二级索引)叶子节点只存主键值,不存完整行数据。当你执行 UPDATE ... WHERE idx_col = ? 且 idx_col 是非唯一辅助索引时,MySQL先在该索引树上定位匹配项,拿到主键值后,必须再去聚簇索引(主键索引)中读取旧值、写入新值——这个“回表”动作是强制的,无法跳过。
所以加锁不是“顺便”,而是语义必需:没锁住主键记录,就无法保证原子性更新;没锁住辅助索引项,就无法阻止并发事务修改同一条件下的其他行(比如插入新记录或改其他字段)。
两把锁必须同时持有,否则会破坏一致性
常见错误认知是“先锁辅助索引,再锁主键索引,中间有时间差”。实际上,在单条 UPDATE 语句的执行过程中,这两把锁是在同一个事务内、由同一线程按固定顺序获取的,但关键在于:它们不是原子打包的,而是分步申请的。这就给并发事务留出了交叉加锁窗口:
- 事务A:通过
idx_status更新某行 → 先持idx_status的X锁,再申请主键X锁 - 事务B:通过
PRIMARY直接更新同一行(如WHERE id = ?)→ 先持主键X锁,再申请idx_status的X锁
一旦A卡在等主键锁、B卡在等辅助索引锁,死锁立即触发。InnoDB检测到后会回滚代价小的那个事务——但问题根源不在检测机制,而在锁获取路径不统一。
没有主键或辅助索引失效时,锁范围会爆炸式扩大
如果 WHERE 条件无法命中任何索引(例如 WHERE name LIKE '%abc%' 且 name 无索引),InnoDB只能全表扫描聚簇索引。每扫一行就对对应主键记录加X锁——哪怕该行最终不满足条件,锁也已加上。结果就是:
- 整张表的主键索引几乎全被锁定(等效“准表锁”)
- RR隔离级别下还会伴随间隙锁,进一步封锁插入点
- 此时哪怕另一事务只是想按某个已有辅助索引更新单行,也会因等待主键锁而长时间阻塞
这种场景下,SHOW ENGINE INNODB STATUS 里看到的 lock struct(s) 数量会远超实际影响行数,且 lock_mode X 出现在大量 PRIMARY 索引页上。
真正容易被忽略的是锁持有时间,而非锁本身
很多死锁日志看起来“就两条简单UPDATE”,却高频发生——根本原因常是事务里混入了外部依赖:一次HTTP调用、Redis写入、甚至日志落盘延迟,都会让本该毫秒级释放的索引锁被持有了几百毫秒。这段时间足够另一个事务完成自己的加锁步骤并反向等待。
比优化SQL更有效的做法,是把业务逻辑中与数据库无关的部分移出事务块;如果必须保留,至少确保所有涉及同一组主键的更新操作,都按升序ID批量执行,从源头收敛加锁路径。











