死锁主因是多二级索引下事务加锁顺序不一致导致环路,而非数据行冲突;典型表现是gap锁与插入意向锁等待,调整索引顺序、删除低频单列索引、优先用VALUES而非SELECT可降低风险。

死锁不是数据冲突,是锁申请顺序不一致
MySQL 的 INSERT 死锁绝大多数不是因为两条语句想插同一行,而是它们在等待对方持有的索引锁——尤其当表有多个二级索引时。InnoDB 会为 INSERT 先加 gap 锁(防止幻读),再加插入意向锁,最后加记录锁;如果两个事务按不同顺序遍历索引树,就可能形成环路。
典型现象:Deadlock found when trying to get lock; try restarting transaction,且 SHOW ENGINE INNODB STATUS 里显示两个事务都在等 lock_mode X locks gap before rec insert intention waiting。
- 只要表有 ≥2 个非唯一二级索引,且并发 INSERT 的 WHERE 条件或 VALUES 涉及不同索引字段组合,风险就明显上升
- 主键或唯一索引上的冲突会直接报
Duplicate entry,不会死锁;死锁只发生在 gap 锁阶段,也就是“还没真正插入但已锁定空隙”时 - 用
INSERT ... ON DUPLICATE KEY UPDATE或REPLACE INTO不会绕过这个问题——它们照样要先走一遍 INSERT 的加锁流程
调整二级索引顺序能降低死锁率,但不能根除
索引定义顺序影响 B+ 树遍历路径和锁的申请次序。把高频用于 WHERE 过滤、且值分布更离散的字段放在复合索引左边,能让 InnoDB 更早剪枝,减少锁住的 gap 范围。
比如表 t_order 有索引 INDEX idx_status_uid (status, user_id) 和 INDEX idx_uid_created (user_id, created_at),并发插入时若一个事务按 status=1 扫描,另一个按 user_id=1001 扫描,二者在两棵索引树上加锁顺序天然不一致。
- 把共用字段(如
user_id)统一提到所有复合索引最左,例如改成idx_uid_status (user_id, status)和idx_uid_created (user_id, created_at),可让多数 INSERT 沿相同路径加锁 - 删除低频、高重复值的单列索引(如只有 3–5 个值的
status索引),它产生的 gap 锁范围大、竞争强,往往是死锁源头 - 注意:索引顺序调整后需同步检查
EXPLAIN中的key_len和rows,避免误伤查询性能
用 INSERT ... SELECT 替代批量 VALUES 时锁更重
INSERT INTO t VALUES (1,'a'),(2,'b'),(3,'c') 是单语句多行,InnoDB 会尽量合并加锁;而 INSERT INTO t SELECT ... FROM other_t 会被当作逐行处理,每行都触发完整锁流程,且无法利用批量优化。
更隐蔽的问题是:如果 SELECT 部分用了 ORDER BY 或 LIMIT,实际执行计划可能改变扫描顺序,进一步放大锁序不一致概率。
- 能用显式
VALUES就别用SELECT—— 即使要从另一张表取数,也优先考虑应用层查出再拼 VALUES - 真要用
INSERT ... SELECT,确保SELECT的 WHERE 条件命中同一个索引,并显式加ORDER BY primary_key统一扫描方向 - 不要在事务里混用
INSERT ... SELECT和后续UPDATE同一张表,这会延长锁持有时间,增加交叉等待机会
唯一约束 + INSERT IGNORE 是最轻量的防重方案
业务上真正需要“不重复插入”的场景,用 INSERT IGNORE 比 ON DUPLICATE KEY UPDATE 或先 SELECT 再 INSERT 更安全。前者在遇到唯一冲突时直接跳过,不升级锁;后两者都会尝试获取记录锁甚至 next-key 锁。
但前提是冲突字段必须走唯一索引(PRIMARY KEY 或 UNIQUE KEY),普通索引无效。
-
INSERT IGNORE失败时返回影响行为 0,不是报错,应用需检查mysql_affected_rows()或cursor.rowcount - 如果业务逻辑要求“存在则更新”,且更新字段不多,可改用
INSERT ... ON DUPLICATE KEY UPDATE col=VALUES(col),比先查后更可靠,但锁开销略大 - 切勿在唯一键上建冗余索引(如已有
UNIQUE(user_id),又建INDEX(user_id, status)),这会让 INSERT 同时给两棵树加锁,显著提升死锁率
真正难处理的是那些既没唯一约束、又必须保证逻辑幂等的场景——这时候得靠应用层分布式锁或业务状态机,数据库层已经帮不上忙了。










