死锁主因是事务加锁顺序不一致而非sql相同;即使语句一致,索引路径不同、间隙锁干扰、未走索引或应用层判断延迟均会导致循环等待。

死锁不是SELECT FOR UPDATE本身干的,而是多个事务对同一组资源加锁顺序不一致导致的循环等待。
为什么相同SQL还会死锁?关键在锁顺序和索引路径
即使两条SELECT ... FOR UPDATE语句完全一样,只要它们执行时走的索引不同、或被其他语句“夹在中间”,就可能触发死锁。InnoDB加锁是按索引树遍历顺序来的,不是按SQL文本顺序。
- 事务A先执行
SELECT * FROM order WHERE order_no = 'O123' FOR UPDATE(走UNIQUE KEY order_no),再执行UPDATE user SET balance = ... WHERE id = 1001 - 事务B反着来:先
UPDATE user ...,再SELECT ... FROM order ... FOR UPDATE - 哪怕两次
SELECT查的是同一行,只要事务间交叉持有对方下一步要锁的资源,InnoDB就会检测到环形等待并回滚其中一个
间隙锁(Gap Lock)在RR级别下是隐形推手
MySQL默认隔离级别REPEATABLE READ会让SELECT ... FOR UPDATE不仅锁匹配的行,还锁住记录之间的间隙——这对防止幻读有用,但极易误伤无关INSERT。
- 表中现有
id = 1, 5, 10,执行SELECT * FROM t WHERE id BETWEEN 3 AND 7 FOR UPDATE会锁住间隙(1,5)和(5,10) - 此时另一个事务执行
INSERT INTO t (id) VALUES (4)会被阻塞,因为它需要Insert Intention Lock,而该锁与间隙锁互斥 - 如果两个事务都查不到目标记录(比如
WHERE id = 5无数据),却都尝试插入id = 5,就会因各自持有对方需要的间隙锁而死锁
autocommit=1 或没走索引,会让锁行为彻底失控
你以为加了FOR UPDATE就锁住了?不一定。很多“死锁”现象其实是锁根本没按预期生效,或者锁得太多太宽,把整个并发链路卡死了。
-
autocommit=1时,SELECT ... FOR UPDATE执行完立刻提交,锁瞬间释放——你后续的UPDATE根本不在锁保护下,等于白加 - WHERE条件没走索引(如
WHERE remark LIKE '%abc'或WHERE DATE(created_at) = ...),InnoDB只能全表扫描,给每一行都加X锁,高并发下直接变成串行处理 - 用
EXPLAIN看type字段:要是ALL或index,说明锁范围远超预期;理想是const或ref,且key列明确显示索引名
别在应用层做条件判断,否则锁持有时间不可控
常见错误模式:SELECT * FROM order WHERE status = 'pending' FOR UPDATE查出100行,再在Java/PHP里遍历判断哪些能处理——这会让锁一直挂着,直到整个循环结束。
- 不同事务遍历结果集顺序不一致(比如没加
ORDER BY id),会导致加锁顺序天然错乱,死锁概率指数级上升 - 正确做法是把业务逻辑压进WHERE:改用
SELECT id FROM order WHERE status = 'pending' AND created_at - 若需关联判断(如用户是否冻结),优先建覆盖索引+主键回查,而不是
JOIN后在应用层过滤
最常被忽略的一点:死锁日志里报的SQL,往往不是问题根源,而是“最后一个被堵住”的语句。真正要盯的是锁等待链中最早那条没走索引、或跨表顺序混乱的SELECT FOR UPDATE——它像多米诺骨牌的第一张,倒了,后面全跟着翻。











