INSERT vs SELECT ... FOR UPDATE 锁机制完整对比总结
一、锁全景对比表
| 维度 | INSERT |
SELECT ... FOR UPDATE |
|---|---|---|
| 核心目的 | 插入新数据,保证唯一性与数据完整性 | 锁定已有数据,防止并发修改/删除,并避免幻读 |
| 表级意向锁 | ✅ 加 IX(意向排他锁),必加 | ✅ 加 IX(意向排他锁),必加 |
| 自增锁 | ✅ 可能有(表级 AUTO-INC 锁或轻量级互斥锁),取决于 innodb_autoinc_lock_mode 和语句类型 |
❌ 无 |
| 间隙锁 | 通过 插入意向锁 (X, GAP, INSERT_INTENTION)标记间隙,仅用于协调并发插入 |
根据隔离级别和查询条件,可能加 间隙锁 (X, GAP)或 Next-Key 锁 (X),用于阻止幻读 |
| 记录锁 | ✅ 插入成功后加 排他记录锁 (X, REC_NOT_GAP),锁定新行 |
✅ 对扫描到的每一行加 排他记录锁 (X, REC_NOT_GAP) |
| 锁定对象 | 新插入的行 + 插入位置的间隙意向 | 查询条件匹配的所有现有行 + 查询范围覆盖的间隙(取决于条件) |
| 锁范围 | 极窄:仅新行及其间隙意向 | 可能很宽:全表、范围或单行(取决于索引使用情况) |
| 对并发插入的影响 | 包容:同一间隙不同位置的并发 INSERT 互不阻塞 |
阻塞:间隙锁会阻止其他事务在锁定范围内插入新行 |
| 对并发更新/删除的影响 | 新行被排他锁保护,其他事务不能修改/删除它 | 锁定的行被排他锁保护,其他事务不能修改/删除它们 |
| 死锁风险 | 较低,但可能在唯一键冲突或间隙锁阻塞时发生 | 较高,尤其是多表/多范围操作 |
| 典型应用场景 | 数据写入、批量导入 | 行级锁定读、乐观锁检查、防超卖 |
二、关键差异深度解析
1. 意向锁(IX)------ 两者相同
- 都加表级 IX 锁 :在任何行级写操作(
INSERT、UPDATE、DELETE、SELECT ... FOR UPDATE)之前,InnoDB 都会自动在表上加 IX 锁。 - 作用 :声明事务将要修改某些行,用于快速判断与表级锁(如
LOCK TABLES ... WRITE)的冲突。 - 兼容性:多个 IX 锁之间完全兼容,不阻塞行级并发。
2. 自增锁(AUTO-INC)------ INSERT 特有
- 何时出现 :仅当表包含
AUTO_INCREMENT列且INSERT需要生成新自增值时。 - 锁类型 :
- 表级自增锁 (
AUTO-INC):在innodb_autoinc_lock_mode = 0(传统模式)或=1(连续模式)下执行INSERT ... SELECT等行数不确定的语句时出现,语句结束后释放。 - 轻量级互斥锁 :在默认模式(
=1)下单行或确定行数的批量插入时使用,非传统锁,不出现于data_locks中。
- 表级自增锁 (
SELECT ... FOR UPDATE:完全不涉及自增锁。
3. 间隙锁与插入意向锁 ------ 本质不同
INSERT |
SELECT ... FOR UPDATE |
|
|---|---|---|
| 锁名称 | 插入意向锁(X, GAP, INSERT_INTENTION) |
间隙锁(X, GAP)或 Next-Key 锁(X) |
| 目的 | 声明"我打算在此间隙插入",允许其他插入意向锁共存 | 声明"此间隙禁止插入",阻塞所有插入意向锁 |
| 兼容性 | 插入意向锁之间兼容 | 间隙锁与任何插入意向锁互斥 |
| 是否锁定记录 | 否(仅间隙意向) | 否(间隙锁不锁记录),但 Next-Key 锁会锁记录 |
简记:插入意向锁是"我要进来",间隙锁是"此路不通"。
4. 行锁(排他记录锁)------ 两者都有,但时机不同
INSERT:插入成功后才加排他记录锁,保护新行。SELECT ... FOR UPDATE:扫描时立即对现有行加排他记录锁,保护读取到的行。
三、锁的兼容性矩阵(行级锁部分)
| 锁类型 | 插入意向锁 | 间隙锁 | Next-Key 锁 | 排他记录锁 |
|---|---|---|---|---|
| 插入意向锁 | ✅ 兼容 | ❌ 冲突 | ❌ 冲突 | ✅ 兼容(不同行) |
| 间隙锁 | ❌ 冲突 | ✅ 兼容 | ✅ 兼容 | ✅ 兼容 |
| Next-Key 锁 | ❌ 冲突 | ✅ 兼容 | ❌ 冲突 | ❌ 冲突 |
| 排他记录锁 | ✅ 兼容 | ✅ 兼容 | ❌ 冲突 | ❌ 冲突 |
说明:✅ 表示两个锁可以同时被不同事务持有;❌ 表示其中一个必须等待另一个释放。
四、验证方法(使用 performance_schema.data_locks)
sql
-- 事务1
BEGIN;
INSERT INTO t (id) VALUES (10); -- 或 SELECT ... FOR UPDATE
-- 事务2(观察锁)
SELECT ENGINE_TRANSACTION_ID, LOCK_TYPE, LOCK_MODE, LOCK_STATUS, LOCK_DATA
FROM performance_schema.data_locks
WHERE OBJECT_NAME = 't';
典型输出差异:
| 操作 | 表级锁 | 行级锁(示例) |
|---|---|---|
INSERT (自增列,模式1) |
TABLE, IX |
RECORD, X,INSERT_INTENTION RECORD, X,REC_NOT_GAP |
SELECT ... FOR UPDATE (范围查询) |
TABLE, IX |
RECORD, X (Next-Key) 或 RECORD, X,GAP |
INSERT ... SELECT (模式0) |
TABLE, IX TABLE, AUTO_INC |
同 INSERT |
五、实际开发建议
- 优先使用
INSERT的天然并发性 :对于单纯的数据写入,不要额外加SELECT ... FOR UPDATE,避免不必要的间隙锁阻塞。 - 需要"防插入"时用
SELECT ... FOR UPDATE:例如实现"唯一库存扣减"前,需锁定范围防止幻读。 - 注意自增锁模式 :高并发写入场景,建议
innodb_autoinc_lock_mode = 2(交错模式),但需确保 binlog 格式为ROW。 - 死锁排查 :
INSERT与SELECT ... FOR UPDATE混合使用时,注意间隙锁与插入意向锁的互斥关系,极易产生死锁。
一句话总结 :
INSERT 加 IX + 插入意向锁 + 排他记录锁 (以及可能的自增锁),主打高并发写入;
SELECT ... FOR UPDATE 加 IX + 间隙锁/Next-Key锁 + 排他记录锁 ,主打强一致锁定读。
两者共享意向锁,但间隙锁与插入意向锁互斥,这是并发冲突与死锁的核心根源。