MySQL 中 INSERT 之所以也会卡在锁上,是因为写入前需要完成唯一性检查、主键分配以及索引间隙维护,这些步骤都可能触发加锁;一旦涉及主键或唯一索引,通常就会出现 Gap Lock 或 Insert Intention Lock。

INSERT 为什么不是“插完就走”,而是卡在锁上?
因为 MySQL 的 INSERT 并不只是简单地往空白位置写一行数据,它在真正写入前还要先做唯一键校验、分配主键值、维护索引间隙范围,而这些操作都需要加锁。你看到的 Lock wait timeout exceeded,很多时候并不是 SQL 执行慢,而是当前 INSERT 被其他事务持有的锁阻塞了。
哪些 INSERT 会主动申请 Gap Lock 或 Insert Intention Lock?
只要数据表中存在主键或唯一索引,INSERT 就必须先判断“这个值是否允许插入”,而这个判断过程往往会涉及某个索引区间。比如准备插入 id=100,就需要确认 95–105 这一段范围内的索引状态是否满足条件。此时就可能触发以下几类锁:
INSERT ... ON DUPLICATE KEY UPDATE:无论最终执行的是插入还是更新,都会先对目标记录或目标间隙申请X lock和insert intention lock- 没走索引的 INSERT(例如 WHERE 条件失效后退化为全表扫描):InnoDB 可能会对较大范围的间隙加锁,导致后续 INSERT 请求持续排队
- 批量 INSERT 的值顺序混乱(如
VALUES (100, 'a'), (99, 'b'), (101, 'c')):事务 A 锁住了 99–100,事务 B 锁住了 100–101,双方就容易互相等待,甚至出现死锁 - 存在外键约束时:INSERT 之前需要先检查父表,并对父表对应记录加 S 锁;如果父表记录正被其他事务更新或删除,当前插入就会被卡住
怎么快速确认是不是锁在 Gap 上?
不要只看 SHOW PROCESSLIST,因为它看不到具体的锁等待细节。更高效的办法是直接联合查询 INNODB_TRX 和 INNODB_LOCK_WAITS:
第一步,先找出当前处于等待状态的事务:
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id FROM information_schema.INNODB_TRX WHERE trx_state = 'LOCK WAIT';
第二步,再查到底是谁在阻塞这些事务:
SELECT blocking_trx_id, requested_lock_id FROM information_schema.INNODB_LOCK_WAITS;
第三步,需要先把 blocking_trx_id 映射为线程 ID,然后到 PROCESSLIST 中定位对应的真实 SQL 语句:
SELECT ID, USER, HOST, COMMAND, TIME, STATE, INFO FROM information_schema.PROCESSLIST WHERE ID = ?;
如果 STATE 显示为 Waiting for table metadata lock,那就说明当前等待的是 DDL 操作(比如 ALTER)持有的元数据锁,而不是 Gap Lock。这种情况要改为排查 performance_schema.metadata_locks。
INSERT 忽略冲突比 ON DUPLICATE KEY UPDATE 更快?
通常是的,而且在高并发场景下差异会比较明显。原因在于两者的执行路径并不一样:
INSERT IGNORE:遇到唯一键冲突时会直接跳过,不对目标行加 X 锁,也不会触发行更新,更不会额外维护二级索引的变更INSERT ... ON DUPLICATE KEY UPDATE:即使UPDATE子句实际上没有修改字段值(例如UPDATE status = status),仍然会对目标记录加 X 锁,并且可能带来索引分裂等额外开销
如果你的业务需求只是“记录已存在就忽略”,那优先考虑 INSERT IGNORE,不要轻易使用 ON DUPLICATE KEY UPDATE——它的本质更接近“先查重、再加锁、再决定是否更新”,整体成本通常明显高于 INSERT IGNORE。
另外,还有两个在 MySQL INSERT 锁等待排查中很容易被忽略的细节:第一,批量插入前最好在应用层先按 ORDER BY id ASC 的顺序整理数据,不要依赖数据库内部排序;即使写了 INSERT SELECT ORDER BY,也不会改变间隙锁的加锁方式。第二,innodb_autoinc_lock_mode = 2 确实能够在一定程度上缓解自增主键的锁竞争,但这个参数通常需要重启后才能生效,并且与 STATEMENT 复制模式不兼容。
