MySQL间隙锁&临键锁

innodb锁分类

sql 复制代码
InnoDB 锁
├── 按粒度分
│   ├── 表锁(Table Lock)
│   │   ├── 意向共享锁(IS)
│   │   ├── 意向排他锁(IX)
│   │   ├── 自增锁(AUTO-INC)
│   │   └── 元数据锁(MDL,Server 层)
│   └── 行锁(Row Lock)
│       ├── 记录锁(Record Lock)      ← 锁具体行
│       ├── 间隙锁(Gap Lock)          ← 锁间隙
│       └── 临键锁(Next-Key Lock)     ← 记录锁 + 间隙锁
│
└── 按功能分
    ├── 共享锁(S Lock):SELECT ... LOCK IN SHARE MODE
    └── 排他锁(X Lock):SELECT ... FOR UPDATE / UPDATE / DELETE

一、间隙锁

1.1 间隙锁是什么

间隙锁(gap lock):间隙锁指的是对索引记录之间的间隙 进行加锁

间隙锁、行锁都是innodb悲观锁的代表

sql 复制代码
索引列 age 的 B+ 树结构:

      [10] ──→ [20] ──→ [30] ──→ [50]
        ↑        ↑        ↑        ↑
      记录1     记录2    记录3    记录4

间隙(Gap):
(-∞, 10)   (10, 20)   (20, 30)   (30, +∞)
   ↑          ↑          ↑          ↑
 间隙1       间隙2       间隙3      间隙4

1.2间隙锁的作用

作用 说明
防止幻读 阻止其他事务在间隙中插入新记录
防止幻读的前提 RR 级别 + 当前读
不锁定具体记录 只锁定记录之间的范围

二、临键锁(RR的核心)

2.1 临键锁定义

临键锁(next-key lock) = 记录锁(Record Lock)+ 间隙锁(Gap Lock)

sql 复制代码
锁定范围:(前一个索引记录, 当前索引记录]
        ↑                        ↑
      开区间                    闭区间

2.2 性能影响

问题 说明
降低并发度 间隙被锁定,插入操作串行化
锁范围扩大 非唯一索引导致锁定范围不可控
死锁检测开销 InnoDB 自动检测死锁,有性能成本

三、哪些语句会加锁

以下语句都是当前读,都会加锁

SQL 类型 加的锁 说明
SELECT ... FOR UPDATE X 锁(临键锁/间隙锁/记录锁) 显式当前读
SELECT ... LOCK IN SHARE MODE S 锁(临键锁/间隙锁/记录锁) 共享当前读
UPDATE ... X 锁(临键锁/间隙锁/记录锁) 隐式当前读
DELETE ... X 锁(临键锁/间隙锁/记录锁) 隐式当前读
INSERT ... 插入意向锁 + 记录锁 特殊处理

关键 :UPDATE 和 DELETE 也是当前读!它们需要先读取最新版本,再加锁修改。

四、什么情况下加什么锁(加锁规则)

4.1 决策流程图(核心规则)

sql 复制代码
当前读语句(FOR UPDATE / UPDATE / DELETE / LOCK IN SHARE MODE)
    │
    ▼
使用的索引类型?
    │
    ├──► 唯一索引(主键/唯一键)
    │       │
    │       ├──► 等值查询 AND 记录存在?
    │       │       └──► 是 → 退化为【记录锁】
    │       │               例:WHERE id = 1(id=1 存在)
    │       │
    │       ├──► 等值查询 AND 记录不存在?
    │       │       └──► 是 → 【间隙锁】
    │       │               例:WHERE id = 5(id=5 不存在)
    │       │               锁定:(上一个存在的id, 下一个存在的id)
    │       │
    │       └──► 范围查询?
    │               └──► 是 → 【临键锁】
    │                       例:WHERE id > 1 AND id < 10
    │
    └──► 非唯一索引(普通索引)
    │       │
    │       └──► 任何查询(等值查询/范围查询) → 【临键锁】
    │               例:WHERE age = 20(age 是普通索引)
    │               锁定:(10, 20] 和 (20, 30)
    │
    └──► 无索引
            └──► 全表扫描 → 所有行加【临键锁】(性能极差)

一句话总结

判断加锁类型的关键是:看 WHERE 条件使用的索引类型 + 查询方式。当前读(UPDATE/DELETE/FOR UPDATE)都会加锁,唯一索引等值命中退化为记录锁,其他情况基本都是临键锁或间隙锁。锁只加在访问的索引上,不同索引之间的锁不冲突。

4.2 具体例子对照

场景 SQL 索引类型 查询方式 加的锁 说明
主键等值命中 UPDATE t SET ... WHERE id = 1 唯一索引 等值 记录锁 id=1 存在,只锁这行
主键等值未命中 UPDATE t SET ... WHERE id = 5 唯一索引 等值 间隙锁 id=5 不存在,锁相邻间隙
主键范围 UPDATE t SET ... WHERE id > 1 AND id < 10 唯一索引 范围 临键锁 锁多行+间隙
普通索引等值 UPDATE t SET ... WHERE age = 20 非唯一索引 等值 临键锁 锁 (10,20] 和 (20,30)
普通索引范围 UPDATE t SET ... WHERE age > 15 AND age < 25 非唯一索引 范围 临键锁 锁 (10,20] 和 (20,30)
无索引 UPDATE t SET ... WHERE name = 'a' 等值 全表临键锁 所有行+间隙
无索引范围 UPDATE t SET ... WHERE name > 'a' AND name < 'z' 范围 全表临键锁 所有行+间隙
插入 INSERT INTO t VALUES (5, 25) --- --- 插入意向锁 检查目标间隙是否有锁

注意,这里有个回表的情况,普通索引查到数据后会回表,如果普通索引等值查询对普通索引列加锁了,那么这条记录对应的主键索引也会被锁住

五、无索引时的全表临键锁与全表加锁的区别

无索引时全表临键锁的效果几乎等同于表锁,但技术上不是真正的表锁

核心区别

维度 无索引时的全表临键锁 真正的表锁(Table Lock)
实现层级 存储引擎层(InnoDB 行锁机制) Server 层或存储引擎层显式锁表
锁定对象 所有行的记录锁 + 所有间隙的间隙锁 整个表的数据和索引
加锁方式 逐行扫描时逐个加临键锁 一次性锁住整张表
锁信息 information_schema.innodb_locks 显示多行 显示为表级锁
死锁检测 参与 InnoDB 死锁检测 不参与(或不同机制)
其他事务 不能插入、删除、更新任何行 不能做任何 DML/DDL

一句话总结

无索引时的全表临键锁,效果上等同于表锁(所有行+所有间隙都被封锁),但实现机制仍是行锁框架下的逐行加锁。真正的性能灾难在于:不仅锁了现有数据,还锁了所有可能的插入位置,导致任何写入操作都无法并发。

六、举例子


复制代码
表结构:id PK(主键索引), age INDEX(普通索引)

数据:
├─ 主键索引(id):[1] → [2] → [3]
│
└─ 普通索引(age):[10] → [20] → [30]
                    ↓
                   指向主键 1, 2, 3

情况 1:事务 A 用普通索引,事务 B 用主键查询同一行

sql 复制代码
-- 事务 A
BEGIN;
SELECT * FROM t WHERE age = 20 FOR UPDATE;
-- 执行过程:
-- 1. 在 age 索引找到 age=20,对应主键 id=2
-- 2. 对 age 索引的 (10,20] 和 (20,30) 加临键锁
-- 3. **回表**:对主键索引 id=2 的记录加 X 锁(记录锁)

-- 事务 B
SELECT * FROM t WHERE id = 2 FOR UPDATE;
-- 直接访问主键索引 id=2
-- 发现 id=2 已有 X 锁(事务 A 加的)
-- ❌ 阻塞!等待事务 A 释放

结论会阻塞! 因为事务 A 回表时也锁了主键索引的记录。


情况 2:事务 A 用普通索引,事务 B 用主键查不同行

sql 复制代码
-- 事务 A
SELECT * FROM t WHERE age = 20 FOR UPDATE;
-- 锁住:age 索引的 (10,20] + (20,30),以及主键 id=2

-- 事务 B
SELECT * FROM t WHERE id = 1 FOR UPDATE;
-- id=1 未被事务 A 锁住
-- ✅ 不阻塞!

SELECT * FROM t WHERE id = 3 FOR UPDATE;
-- id=3 未被事务 A 锁住
-- ✅ 不阻塞!

情况 3:事务 B 用主键查,但事务 A 的锁不包含该主键

sql 复制代码
-- 事务 A
SELECT * FROM t WHERE age = 20 FOR UPDATE;
-- 只锁住 age=20 对应的 id=2

-- 事务 B
SELECT * FROM t WHERE id = 1;
-- id=1 对应 age=10,不在事务 A 的锁定范围内
-- 但注意:事务 A 的间隙锁 (10,20] 包含 age=10 吗?
-- (10,20] = 10 开区间,20 闭区间,即 age > 10 AND age ≤ 20
-- age=10 不在 (10,20] 内
-- ✅ 不阻塞!

情况4:插入到间隙(经典幻读防护)

sql 复制代码
-- 事务 A
BEGIN;
SELECT * FROM t WHERE age > 15 AND age < 25 FOR UPDATE;
  -- 临键锁:(10, 20] 和 (20, 30)

-- 事务 B(会被阻塞)
INSERT INTO t VALUES (5, 18);              -- ❌ 阻塞!18 在 (10,20]
INSERT INTO t VALUES (6, 22);              -- ❌ 阻塞!22 在 (20,30)
INSERT INTO t VALUES (7, 35);              -- ✅ 通过!35 不在锁定范围

关键图示

复制代码
age 索引(普通索引)          主键索引(id)
                             
(10, 20] 临键锁 🔒            id=1  age=10
    │                          ↑
    ▼                         无锁 ✅
   age=20 ───────回表────────► id=2  age=20 🔒 X锁(事务A持有)
    │                          ↑
(20, 30) 间隙锁 🔒            事务B: WHERE id=2 FOR UPDATE
    │                              需要 id=2 的 X 锁
    ▼                              ❌ 阻塞!
   age=30 ───────回表────────► id=3  age=30
                              ↑
                             无锁 ✅

一句话总结

普通索引的查询会回表锁主键,所以用主键查同一行会被阻塞;查不同行则不会。核心判断标准是:最终是否访问到同一行记录的同一索引。

相关推荐
TDengine (老段)2 小时前
TDengine 压缩编码机制 — 双层压缩架构与类型特化算法
大数据·数据库·物联网·算法·时序数据库·tdengine·涛思数据
苏渡苇3 小时前
Redis 持久化——RDB 快照 vs AOF 日志
数据库·redis·缓存·redis持久化·aof vs rdb
l1t4 小时前
DeepSeek总结的使用 PEG 实现运行时可扩展的 SQL 解析器
数据库·sql
这个DBA有点耶4 小时前
COUNT进阶(续):超大表去重计数的极致优化
数据库·架构·代码规范
爱喝水的鱼丶4 小时前
SAP-ABAP:SAP 简单报表输出开发系列(共6篇) 第四篇:SAP 报表异常处理机制:数据校验与消息提示规范落地
开发语言·数据库·学习·算法·sap·abap
_1_74 小时前
SQL SERVER闪退问题解决
数据库·sqlserver
ZengLiangYi4 小时前
sql.js WASM 深度解析
javascript·数据库·后端
一 乐5 小时前
人口老龄化社区服务与管理平台|基于springboot+vue的人口老龄化社区服务与管理平台(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·人口老龄化社区服务与管理平台
梓䈑5 小时前
【MySQL】表的操作(数据表的创建、查看 和 修改)
数据库·mysql