死锁主因是事务加锁顺序不一致,而非SQL错误;优化索引可缩小锁范围、统一执行路径,但必须匹配WHERE/JON过滤逻辑,且需配合统一锁序与缩短事务才能根治。

直接结论:死锁报错本身不说明SQL写错了,而是多个事务在争抢同一组行时加锁顺序不一致;优化索引顺序能缩小锁范围、统一执行路径,从而大幅降低死锁概率——但前提是索引设计必须匹配 WHERE 和 JOIN 的实际过滤逻辑。
为什么联合索引字段顺序直接影响死锁频率
MySQL 的行锁加在索引记录上,而执行计划是否走索引、走哪个索引、锁多少行,全由索引结构和 WHERE 条件的匹配度决定。字段顺序不对,会导致:索引失效 → 全表/全索引扫描 → 锁住大量无关行 → 多个事务锁集重叠概率飙升。
-
(pool_id, identifier, status, is_redeemed)这类索引,如果identifier允许为 NULL,InnoDB 在等值查询WHERE pool_id = ? AND status = ?时可能无法跳过identifier直接用后缀字段,最终退化为只用pool_id前缀,引发间隙锁扩散 - 正确顺序应把高频等值条件放最左:比如业务里总是先查
pool_id再筛status,那就该建(pool_id, status, is_redeemed, identifier)——让 WHERE 条件严格按序命中前缀 - 范围查询(如
created_at > '2024-01-01')必须放在联合索引末尾,否则后续字段无法被索引驱动
UPDATE/DELETE 语句没走索引是死锁高发根源
没有索引的 UPDATE 会触发全表扫描,InnoDB 可能对每行都加 X 锁(甚至升级为表锁),不同事务扫描顺序不同,极易形成交叉加锁。这不是“偶尔撞上”,而是每次执行都在扩大死锁面。
- 检查执行计划:
EXPLAIN UPDATE ...看key列是否为NULL或非预期索引 - 避免在索引列上做函数操作:
WHERE ABS(score) > 80会让score索引完全失效,应改写为score > 80 OR score < -80 - JOIN 场景下,ON 和 WHERE 条件共同决定索引选择。例如
JOIN order_items ON a.id = b.order_id WHERE b.created_at > ?,order_items上必须有(order_id, created_at)联合索引,单建created_at索引毫无意义
如何验证索引调整是否真正降低死锁
别只看慢查询日志或 EXPLAIN 输出,死锁是否缓解,得看真实并发下的锁行为是否收敛。
- 开启持久化死锁日志:
innodb_print_all_deadlocks = ON,并确认log_error路径可写——否则你永远只能看到最后一次死锁 - 复现时抓
SHOW ENGINE INNODB STATUS\G中的LATEST DETECTED DEADLOCK,重点比对两件事:TABLE和INDEX是否相同、lock_mode是否从X locks gap before rec(间隙锁)变成X locks rec but not gap(纯行锁) - 压测前后对比锁等待数:
SELECT * FROM performance_schema.data_locks;(MySQL 8.0+)或INFORMATION_SCHEMA.INNODB_TRX中TRX_WAITING_LOCK_ID非空事务数
最容易被忽略的一点:索引调优只是“让锁更准”,但无法解决“锁顺序冲突”这个根本问题。哪怕所有 SQL 都走最优索引,只要两个事务一个先更新 user 再更新 order,另一个反过来,死锁依然会发生。所以索引必须和事务内表操作顺序、业务层资源访问约定一起落地,缺一不可。


















