mysql 5.7批量插入死锁根本原因是唯一索引上并发检查与加锁顺序不一致导致循环等待;必须通过show engine innodb status\g的latest detected deadlock定位全貌,结合innodb_trx查持锁休眠事务、innodb_locks比对间隙锁与插入意向锁的物理冲突点。

MySQL 5.7 中批量插入(如 INSERT ... ON DUPLICATE KEY UPDATE 或 REPLACE INTO)引发的死锁,根本原因不是“插得太快”,而是多个事务在唯一索引上并发检查、加锁顺序不一致导致的循环等待。直接查 INNODB_LOCK_WAITS 常常为空或误导,必须交叉验证三处:死锁日志、INNODB_TRX、INNODB_LOCKS。
看 SHOW ENGINE INNODB STATUS\G 里的 LATEST DETECTED DEADLOCK
这是唯一能确认死锁全貌的入口,不能跳过。重点抓三块:
-
LATEST DETECTED DEADLOCK段落里两个事务的trx_id和各自Locks held/Locks waiting—— 注意它写的“holds X lock on record”或“holds S lock on gap before rec”不是业务主动加的,而是ON DUPLICATE KEY过程中为唯一性检查隐式持有的 - 每个事务的
mysql thread id和query,用来反查应用侧是哪条 SQL 触发的 - 末尾的
*** WE ROLL BACK TRANSACTION trx_id...表明 MySQL 已自动回滚哪个事务,但没告诉你为什么选它——这得靠下一步判断
查 INNODB_TRX 找出“休眠但持锁”的元凶
死锁发生时,INNODB_TRX 往往藏着真凶:那些 trx_state = 'RUNNING' 且 COMMAND = 'Sleep' 的事务。它们已执行完 SQL,但没 COMMIT 或 ROLLBACK,锁一直挂着。
- 执行:
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_state = 'RUNNING' AND COMMAND = 'Sleep' ORDER BY trx_started ASC - 越早启动的
Sleep事务越危险——它可能正拿着某条记录的 X 锁,而新来的批量插入在等它的间隙锁释放 - 特别注意:这类事务常来自连接池未正确归还连接(比如 try-catch 后忘了 commit),不是 SQL 写错了,是应用层逻辑漏了
用 INNODB_LOCKS 定位物理锁冲突点
INNODB_LOCK_WAITS 在 5.7 中对间隙锁(GAP)和插入意向锁(INSERT_INTENTION)基本失效,必须手动比对 INNODB_LOCKS 的物理定位字段。
- 关键字段组合是:
lock_space+lock_page+lock_rec—— 三者完全相同才表示锁在同一行/同一间隙 - 如果事务 A 持有
lock_mode = X,lock_type = RECORD,lock_index = 'songId_idx';事务 B 等待lock_mode = X,lock_type = INSERT_INTENTION, 同一lock_space/lock_page但lock_rec不同 → 这就是典型的 GAP + 插入意向锁死锁 - 不要只看
lock_mode是 X 还是 S,INSERT ... ON DUPLICATE KEY UPDATE在 RR 隔离级别下会先持 S 锁检查唯一性,再升级为 X 锁写入,中间状态极易被截断
为什么 INNODB_LOCK_WAITS 查不到等待链
因为 INNODB_LOCK_WAITS 只映射 RECORD 级别的直接阻塞,而批量插入死锁常发生在 GAP 锁与 INSERT_INTENTION 锁之间,后者在 5.7 的该表中不暴露等待关系。
- 现象:
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS返回空,但SHOW ENGINE INNODB STATUS明确写了 “waiting for this lock to be granted” - 根源:插入意向锁本质是 GAP 锁的一种,
INNODB_LOCK_WAITS不记录 GAP 层级的等待,只记录 RECORD 层级的显式阻塞 - 绕过方法:从死锁日志提取两个事务的
trx_id,分别去INNODB_LOCKS查它们的lock_trx_id、lock_space、lock_page,手动比对是否构成“你锁我等的间隙,我锁你等的记录”这种交叉模式
真正难排查的不是锁本身,而是“谁在持有锁却不干活”——那个 trx_state = 'RUNNING' 却 COMMAND = 'Sleep' 的事务,往往藏在应用日志深处,需要结合线程 ID 反查服务端 trace。别只盯着 SQL,先盯住连接生命周期。











