INSERT ... ON DUPLICATE KEY UPDATE 更适合高并发写入,因其为原子操作,通过引擎层行锁串行化冲突路径,避免“读-判-写”窗口期导致的重复插入或覆盖丢失。

为什么 INSERT ... ON DUPLICATE KEY UPDATE 比 SELECT + INSERT/UPDATE 更适合高并发写入
因为前者是原子操作,避免了“读-判-写”窗口期引发的重复插入或覆盖丢失。在并发场景下,SELECT 查不到记录 → 多个事务同时走 INSERT → 唯一键冲突;或者查到记录 → 多个事务都执行 UPDATE → 后写覆盖前写。而 ON DUPLICATE KEY UPDATE 由 MySQL 在引擎层加行锁(针对冲突的唯一索引值),天然串行化冲突路径。
实操建议:
- 确保冲突字段上有唯一索引(主键或
UNIQUE约束),否则不触发更新逻辑 - 避免在
UPDATE子句中引用未在VALUES()中提供的列,否则可能产生非预期空值 - 若需根据原值做计算(如计数器自增),用
col = col + 1而非col = VALUES(col) + 1,后者会取本次插入值而非当前行值
INSERT INTO ... SELECT 在并发写入时为何容易锁表或死锁
该语句默认对源表(SELECT 部分)加一致性读锁(MVCC),但目标表(INSERT INTO)会按实际写入顺序逐行加 INSERT intention lock 和行锁。当多个事务同时执行类似语句,且涉及相同主键/唯一键范围时,容易因加锁顺序不一致导致死锁;若源表无合适索引,还可能升级为间隙锁(gap lock),阻塞其他范围写入。
实操建议:
- 源表
SELECT条件必须命中索引,避免全表扫描触发大范围 gap lock - 目标表主键或唯一键字段务必有索引,否则
INSERT过程无法准确定位冲突行,退化为更重的锁机制 - 考虑拆成小批量(如
LIMIT 1000+ 循环),降低单次事务持有锁的时间
如何用 innodb_lock_wait_timeout 和死锁日志定位真实瓶颈
单纯调高 innodb_lock_wait_timeout(默认 50 秒)只是掩盖问题,真正要解决的是锁等待源头。MySQL 的 SHOW ENGINE INNODB STATUS 输出中,LATEST DETECTED DEADLOCK 段落会明确列出两个事务各自持有的锁、等待的锁、SQL 语句及加锁的索引记录 —— 这比应用层报错信息更精准。
实操建议:
- 在业务 SQL 中显式指定锁粒度:需要强一致性时用
SELECT ... FOR UPDATE,仅防幻读可用SELECT ... LOCK IN SHARE MODE - 检查事务中是否有长耗时操作(如远程调用、大循环),它们会拉长锁持有时间,放大竞争概率
- 确认是否误用了
REPEATABLE READ隔离级别下的间隙锁:若业务允许,可降级为READ COMMITTED(需关闭binlog_format = 'STATEMENT')
批量写入时 INSERT 语句合并与事务大小的平衡点
单条 INSERT 开销大(网络往返 + 日志刷盘),但事务过大(如 10 万行)会导致 undo log 膨胀、锁持有时间过长、主从延迟加剧。经验值是每事务 1k–5k 行,具体取决于单行数据大小和磁盘 I/O 能力。
实操建议:
- 用
INSERT INTO t VALUES (...), (...), (...)合并多行,而非多次单行INSERT - 禁用自动提交(
SET autocommit = 0),手动控制COMMIT时机 - 避免在事务中混用 DDL(如
ALTER TABLE),它会隐式提交当前事务并锁全表
INSERT INTO orders (user_id, amount, status) VALUES (1001, 99.9, 'paid'), (1002, 129.5, 'paid'), (1003, 59.0, 'pending');
真正难处理的不是锁本身,而是不同业务逻辑对“一致性”的定义差异——比如库存扣减要求绝对精确,而日志写入允许短暂重复。把锁策略和业务语义对齐,比调参重要得多。










