死锁本质是多事务对同一组资源以不同顺序加排他锁导致环形等待,与单条sql正确性无关,而取决于执行路径、索引命中、隔离级别及并发时序。

死锁不是SQL写得不对,而是事务加锁顺序不一致
MySQL死锁本质是多个事务以不同顺序申请同一组资源的排他锁(X锁),最终形成环形等待。它和单条SQL是否“正确”无关,而取决于事务中多条语句的执行路径、索引命中情况、隔离级别,以及并发时的时序竞争。
常见诱因包括:
- UPDATE/DELETE 语句未走索引,触发全表扫描 → 锁住大量无关行
- 事务中先 UPDATE A 再 UPDATE B,另一个事务却先 UPDATE B 再 UPDATE A
- SELECT ... FOR UPDATE 在非唯一索引上范围扫描,锁住间隙(Gap Lock)
- 高并发下 INSERT ... ON DUPLICATE KEY UPDATE 或 REPLACE INTO 触发隐式锁升级
用 EXPLAIN 和 INFORMATION_SCHEMA 查锁前先看执行计划
死锁日志(SHOW ENGINE INNODB STATUS)只告诉你“谁被回滚了”,但不告诉你是哪条SQL引发的锁竞争。真正要定位,得提前确认每条语句实际加了什么锁:
- 对所有 UPDATE/DELETE/SELECT ... FOR UPDATE,先跑
EXPLAIN FORMAT=JSON,重点看key、rows_examined、using_index_condition是否为true - 检查 WHERE 条件是否命中**最左前缀索引**;若用
OR、函数包裹字段(如WHERE YEAR(create_time) = 2024),很可能退化为全表扫描 - 在可重复读(RR)下,非唯一二级索引的范围查询会加
Gap Lock,可用SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX+INNODB_LOCKS(MySQL 5.7)或performance_schema.data_locks(8.0+)实时观察
INSERT ON DUPLICATE KEY UPDATE 是高危操作
这个语法看似原子,实则分三步:尝试插入 → 冲突时加 S 锁 → 升级为 X 锁再更新。在并发插入相同唯一键时,极易因锁等待顺序不一致导致死锁,尤其当唯一键是非主键索引时。
更稳妥的做法:
- 确保冲突字段是**主键或有唯一索引**,避免锁扩散
- 把
INSERT ... ON DUPLICATE KEY UPDATE拆成两步:先SELECT ... FOR UPDATE判断是否存在,再决定 INSERT 或 UPDATE(需包在同一个事务里) - 如果只是计数类场景(如统计UV),考虑用
INSERT IGNORE+ 应用层重试,避开锁升级路径
事务粒度越小越好,但不能靠 sleep() 强行错开
缩短事务时间确实能降低死锁概率,但“在UPDATE后加 SLEEP(0.1)”这种做法不可取——它延长了锁持有时间,反而增加竞争窗口。关键在于:
- 把 SELECT 查询提前到事务外,只在必要时才开启事务
- UPDATE 语句尽量带确定性条件,避免
WHERE status = 'pending'这种可能匹配千行的模糊条件 - 业务上允许时,用乐观锁(如
UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?)替代悲观锁 - 死锁发生后,应用必须捕获
Deadlock found when trying to get lock错误并自动重试(通常 2–3 次足够)
真正难处理的,是那些依赖多张表、跨服务、锁范围不透明的复合事务——这时候光调SQL没用,得从领域建模和接口契约层面收敛锁边界。










