根本原因是innodb在repeatable read下对delete加gap锁,与insert的插入意向锁冲突形成循环等待;需确保user_id有索引、避免隐式转换,并优先用insert...on duplicate key替代。

为什么先 DELETE 再 INSERT 容易死锁
根本原因不是“删+插”这个动作本身,而是 InnoDB 在 REPEATABLE READ 隔离级别下对范围的加锁行为。当你执行 DELETE FROM user_relation WHERE user_id IN (...),InnoDB 会为匹配条件的每条记录加 X 锁,并为这些记录之间的间隙加 GAP 锁(防止幻读)。紧接着的 INSERT INTO user_relation 又需要在相同索引位置尝试获取插入意向锁(insert intention lock),而该意向锁与 GAP 锁冲突——事务 A 持有某段 GAP 锁,事务 B 想插进去,但 B 的插入意向锁被 A 阻塞;同时 B 也持有了另一段 GAP 锁,反过来阻塞 A 的 INSERT。这就形成了典型的循环等待。
检查 user_relation 表的索引是否覆盖 WHERE 和 INSERT 条件
死锁高频发生,90% 以上是因为 user_id 字段没建索引,或复合索引顺序不匹配。InnoDB 行锁只在索引上生效;没有索引时,DELETE WHERE user_id IN (...) 会退化为全表扫描 + 表级锁倾向,极大提高冲突概率。
- 运行
SHOW CREATE TABLE user_relation;,确认user_id是否为主键或有独立索引;若没有,立即加:ALTER TABLE user_relation ADD INDEX idx_user_id (user_id); - 如果用的是复合索引(如
(user_id, relation_id, relation_type)),确保DELETE和后续INSERT的 WHERE 条件能命中该索引最左前缀;否则 GAP 锁范围不可控 - 避免在
WHERE中混用类型:比如user_id是BIGINT,但传入字符串'123',会导致隐式转换、索引失效
用 REPLACE INTO 或 INSERT ... ON DUPLICATE KEY UPDATE 替代 DELETE+INSERT
这是最直接规避 GAP 锁冲突的方式。它把“删旧+插新”压缩成单次原子操作,InnoDB 对同一主键/唯一键的重复插入只会加 X 锁 + Next-Key 锁,不会产生间隙锁竞争。
- 前提:
user_relation表必须有主键或唯一索引(如UNIQUE KEY (user_id, relation_id)),否则ON DUPLICATE KEY无意义 - 示例替换:
INSERT INTO user_relation (user_id, relation_id, relation_type) VALUES (...), (...) ON DUPLICATE KEY UPDATE relation_type = VALUES(relation_type);
- 注意:如果业务逻辑依赖
DELETE触发器(如级联清理日志),改用INSERT ... ON DUPLICATE KEY后需手动补触发逻辑
降低隔离级别或显式控制锁范围
REPEATABLE READ 是 GAP 锁的温床;但在多数业务场景中,READ COMMITTED 完全够用,且能彻底关闭 GAP 锁和 Next-Key 锁——只锁实际命中的行,不锁间隙。
- 临时修改会话级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; - 更稳妥的做法是在应用层连接池配置中统一设置(如 JDBC URL 加
&transactionIsolation=TRANSACTION_READ_COMMITTED) - 如果无法降级,至少避免在事务中做无关操作:删除、插入之间不要穿插
SELECT、远程调用或耗时计算,否则锁持有时间拉长,死锁概率指数上升
DELETE 语句看着走了索引,实则因参数类型不匹配或函数包裹(如 WHERE CAST(user_id AS CHAR) = '123')导致全表扫描——这种问题在线上日志里几乎不报错,只默默制造死锁。











