位置:首页 > SQL > SQL 高并发插入时如何避免死锁?

SQL 高并发插入时如何避免死锁?

时间:2026-08-23  |  作者:星河游者  |  阅读:0

目录

  1. 为什么优先用 INSERT ... ON DUPLICATE KEY UPDATE
  2. 高并发插入的死锁,通常卡在先查后插
  3. 必须用 SELECT FOR UPDATE 时,至少满足这三点
  4. 排查死锁时,别只盯着正在写入的主表
  5. 实战结论:按这个顺序做判断

前言

订单号、流水号这类高并发写入场景里,很多死锁并不是出在 `INSERT` 本身,而是出在“先查后插”这套看似稳妥的流程。下面按实际排障的思路拆开说明:什么时候应直接用 `INSERT ... ON DUPLICATE KEY UPDATE`,什么时候 `SELECT FOR UPDATE` 会把锁范围放大,以及保留行锁方案前必须核对哪些条件。

订单、流水号这类写入场景一上并发,很多团队第一反应是“先查一遍,再决定插入还是更新”。问题恰恰出在这里:高并发插入本身通常不是死锁源头,真正危险的是事务里先做 SELECT ... FOR UPDATE,再配上未命中索引的条件或额外的跨表操作。

这篇文章把判断顺序拆开说清楚:先看为什么 INSERT ... ON DUPLICATE KEY UPDATE 通常是首选,再看 SELECT FOR UPDATE 在插入场景下为什么容易把锁链拉复杂,最后给出必须使用它时的最低安全条件,方便你快速判断当前写法该保留还是该改。

为什么优先用 INSERT ... ON DUPLICATE KEY UPDATE

INSERT ... ON DUPLICATE KEY UPDATE 的核心优势,不是语法简洁,而是它把“存在则更新、不存在则插入”压缩成一条原子语句。整个过程不依赖应用层先查询再判断,也不需要显式加锁,更适合高并发写入。

展示 INSERT ... ON DUPLICATE KEY UPDATE 在唯一索引下如何完成原子写入,以及 ROW_COUNT() 的返回含义。
高并发写入为什么优先选 Upsert把“存在则更新、不存在则插入”收敛为单条原子语句,是高并发写入里更稳妥的默认方案。

前提是冲突字段必须建立唯一约束。例如 order_no 需要有 UNIQUEPRIMARY KEY 索引。这样 InnoDB 才能借助意向插入锁与唯一约束检测,在并发冲突时直接走数据库内部的冲突处理流程,而不是在业务层拼接出一条容易形成死锁的锁等待链。

这种写法生效的前提

  • 冲突字段如 order_no 必须建有 UNIQUEPRIMARY KEY 索引,否则 ON DUPLICATE KEY UPDATE 不会按预期工作。
  • 不要在同一条语句里同时依赖多个唯一键处理冲突,MySQL 只会按第一个匹配到的唯一键触发 UPDATE
  • 如果业务需要知道这次到底是插入还是更新,可以用 ROW_COUNT() 判断:返回 1 表示插入,返回 2 表示更新。
INSERT INTO orders (order_no, status)
VALUES ('ABC', 'NEW')
ON DUPLICATE KEY UPDATE
status = VALUES(status);

高并发插入的死锁,通常卡在先查后插

很多项目里真正的问题写法,不是直接插入,而是先做“插入前校验”:先查记录在不在,再决定后续逻辑。这种模式一旦和 SELECT ... FOR UPDATE 绑定,就很容易把简单写入变成复杂锁竞争。

尤其在记录不存在、条件没走索引、事务中还有后续插入或更新时,死锁往往不是偶发,而是并发一上来就反复出现。

SELECT FOR UPDATE 容易死锁的三个原因

  • 当查询查不到记录时,例如 SELECT * FROM orders WHERE order_no = 'ABC' FOR UPDATE,InnoDB 可能锁住对应的间隙,也就是 gap lock。多个并发请求如果都卡在同一个间隙上,后续插入就会互相等待。
  • 如果 WHERE 条件字段没有索引,例如按 status 查询,MySQL 可能把锁范围放大,严重时等价于高并发下排队抢一把大锁。
  • 事务里紧接着再执行 INSERT,插入意向锁与前面的间隙锁发生冲突,就可能形成 A 等 B、B 等 A 的闭环,最终触发死锁。
SELECT *
FROM orders
WHERE order_no = 'ABC'
FOR UPDATE;

必须用 SELECT FOR UPDATE 时,至少满足这三点

SELECT FOR UPDATE 不是完全不能用,但在插入场景里,只有满足明确约束时才算安全可用。少一个条件,风险都会明显上升。

展示必须使用 SELECT FOR UPDATE 时的最低安全条件,包括 EXPLAIN 命中唯一索引、避免跨表锁序和应用层重试。
行锁方案的最低安全门槛如果业务确实离不开 SELECT FOR UPDATE。

最低安全条件

  • 先用 EXPLAIN 验证执行计划,确认 typeconstref,并且 rows = 1。这意味着查询命中唯一索引,锁范围基本可控在单行。
  • 事务内不要夹杂其他表操作,例如查完订单又去更新用户余额。跨表之后,锁顺序更难统一,死锁概率会明显上升。
  • 应用层必须实现幂等重试。遇到 Deadlock found when trying to get lock 后,应回滚并重试,常见做法是指数退避,例如 sleep(0.05 * 2^i)
EXPLAIN SELECT *
FROM orders
WHERE order_no = 'ABC'
FOR UPDATE;

排查死锁时,别只盯着正在写入的主表

一个很容易被忽略的现象是:死锁日志里反复出现的表,未必就是你当前正在插入的那张表。很多时候,真正把锁链拉长的是被 JOIN、子查询或事务内额外语句带进来的关联表。

所以排查时不能只优化主 SQL。更有效的做法,是把所有嵌套查询和关联表都过一遍,确认它们是否都走了索引、是否扩大了锁范围、是否改变了原本简单的锁顺序。实践里,这一步往往比单纯调整插入语句更关键。

实战结论:按这个顺序做判断

如果业务本质上只是“有则更新、无则插入”,优先改成 INSERT ... ON DUPLICATE KEY UPDATE,并确保冲突字段上有 UNIQUEPRIMARY KEY 索引。这通常是高并发写入里最省事、也最稳的方案。

如果确实必须保留 SELECT FOR UPDATE,就先检查三件事:是否命中唯一索引、事务里是否混入其他表操作、应用层是否具备幂等重试能力。只要这三项里有一项做不到,就应该优先回头重构“先查后插”的流程,而不是继续在死锁参数上做小修小补。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多