vault backup: 2026-05-21 22:11:56

This commit is contained in:
hhs
2026-05-21 22:11:56 +08:00
parent 9fc46eac24
commit c3779073e9
5 changed files with 700 additions and 263 deletions
@@ -50,11 +50,27 @@ graph BT
style NKLock fill:#00B6BC,color:#fff
```
### 场景矩阵 — 查询条件 → 锁类型速查
```mermaid
flowchart TD
Q{"查询方式"}
Q -->|"精确等值(unique index)"| R1["Record Lock<br/>仅锁那条记录"]
Q -->|"精确等值(non-unique index)"| R2["Next-Key Lock<br/>记录 + 左侧间隙"]
Q -->|"范围查询<br/>WHERE id > 10"| R3["每个匹配记录的<br/>Next-Key Lock + 最大值右侧间隙"]
Q -->|"无索引条件"| R4["全表记录的 Next-Key Lock<br/>退化为表锁效果 ⚠️"]
Q -->|"DELETE | UPDATE | INSERT"| R5["INSERT: Gap Lock on gap<br/>DELETE/UPDATE: Record Lock"]
style R1 fill:#00B6BC,color:#fff
style R4 fill:#EE5A24,color:#fff
style R5 fill:#FF9F43,color:#000
```
> [!NOTE] 为什么需要三张图?
>
> - **第一张**展示锁的粒度层次:从全局→表→行
> - **第二张**展示行锁的子类关系:三种基础锁如何组合成临键锁
> - **第三张**(下方场景矩阵)展示不同查询条件下 InnoDB 实际选择哪种锁
> - **第三张**展示不同查询条件下 InnoDB 实际选择哪种锁(场景矩阵)
>
> 三者互为补充,共同构成完整的锁选型全景。带着这三张图,你可以回答面试中「InnoDB 锁到底有哪些类型」的问题了。
@@ -185,22 +201,58 @@ SELECT * FROM users WHERE id = 1 FOR UPDATE;
### 意向锁的作用
意向锁解决的核心问题是:**当一个事务想加表级锁时,怎么快速知道"表里有没有人在锁行"?**
没有意向锁 → 必须逐行扫描(百万行代价太高)。
有了意向锁 → 看一眼表级标记就知道。
#### 场景一:行级加锁流程
事务想对某行加锁时,InnoDB **自动**在表级申请意向锁:
```mermaid
flowchart TD
A["事务 A 想在行级加 X 锁"] --> B{"检查表级意向锁"}
B --> C{"表中是否有其他事务的<br/>意向共享锁 IS?"}
C -->|有| D["冲突!不能同时拥有 IX + IS"]
B -->|无| E{"表中是否有其他事务的<br/>意向排他锁 IX?"}
E -->|有| F["冲突!表级不支持多个 IX"]
E -->|无| G["✅ 放行,加行级 X 锁"]
style G fill:#00D866,color:#fff
style D fill:#EE5A24,color:#fff
style F fill:#EE5A24,color:#fff
A["事务想在行级加 X 锁"] --> IX["🔒 自动在表级加 IX 锁<br/>(声明:我打算锁表里的某些行)"]
IX --> B{"表级是否有<br/>S 锁或 X 锁?"}
B -->|"有"| C["⏳ 等待表级锁释放"]
B -->|"无"| D["✅ IX 加锁成功<br/>→ 继续在行级加 X 锁"]
style D fill:#00D866,color:#fff
style C fill:#EE5A24,color:#fff
style IX fill:#FF9F43,color:#000
```
> [!NOTE] 意向锁不是用户能控制的
> 当事务准备对某行加 S 锁时,InnoDB 自动在表级加 IS;加 X 锁时自动加 IX。它的作用是快速判断「这张表有没有人在用」,而不必逐行扫描。
> [!NOTE] IS 和 IX 之间、IX 和 IX 之间都**不冲突**
> 因为意向锁只是"声明意图",真正的冲突发生在**行级**。两个事务各自改不同行,表级意向锁完全可以共存。
>
> | 已持有 ↓ \ 请求 → | IS | IX | S | X |
> |---|---|---|---|---|
> | **IS** | ✅ | ✅ | ✅ | ❌ |
> | **IX** | ✅ | ✅ | ❌ | ❌ |
> | **S** | ✅ | ❌ | ✅ | ❌ |
> | **X** | ❌ | ❌ | ❌ | ❌ |
#### 场景二:表级加锁流程(意向锁的真正用途)
当有人想给整张表加 S 锁或 X 锁时,**只需检查表级意向锁**,不用逐行扫描:
```mermaid
flowchart TD
A["事务想给表加<br/>S 锁 / X 锁"] --> Q{"表级有没有 IX 锁?<br/>(有人在做行级写入)"}
Q -->|有| B["⏳ 等待 IX 释放<br/>(说明有人在改行数据)"]
Q -->|无| R{"表级有没有 IS 锁?<br/>(有人在做行级读取)"}
R -->|"X 锁请求 + 有 IS"| B2["⏳ 等待 IS 释放"]
R -->|"S 锁请求 + 有 IS"| C["✅ IS 兼容 S 锁,放行"]
R -->|无| D["✅ 放行,加表级锁"]
style D fill:#00D866,color:#fff
style C fill:#00D866,color:#fff
style B fill:#EE5A24,color:#fff
style B2 fill:#EE5A24,color:#fff
```
> [!TIP] 一句话记住
> 意向锁的真正价值不在于阻塞其他意向锁,而在于**让表级锁的兼容性判断变成 O(1)**——只看标记,不扫行。
## 记录锁、间隙锁、临键锁
@@ -231,12 +283,35 @@ SELECT * FROM orders WHERE status = 'pending' FOR UPDATE;
> [!QUESTION] Gap Lock 和 Record Lock 最大的区别是什么?
> Record Lock 锁的是"已经存在的东西",Gap Lock 锁的是"还不存在但可能出现的东西"。一个是保护现在,一个是防止未来。
> [!QUESTION] 那 InnoDB 怎么知道该锁哪个间隙?
> 关键在于 B+ 树索引是**有序**的。当查询值不存在时,InnoDB 在索引上做查找,找到**刚好包围这个值的左右两条记录**——左边是 ≤ 查询值的最大记录,右边是 > 查询值的最小记录。这两条记录之间的区间,就是 InnoDB 要锁的间隙。
>
> 一句话总结:**"谁包围了这个值,间隙就是谁"**。
索引上的值 `10, 20, 30, 40, 50` 把整条数轴切成了若干个间隙,目标值落在哪个间隙,就锁哪个:
```mermaid
graph LR
subgraph Gaps["索引记录切分出的间隙"]
direction LR
G0["(-∞, 10)"] --- R1["10"] --- G1["(10, 20)"] --- R2["20"] --- G2["(20, 30)"] --- R3["30"] --- G3["(30, 40)"] --- R4["40"] --- G4["(40, 50)"] --- R5["50"] --- G5["(50, +∞)"]
end
TARGET["查询 id=25"] -.->|25 落入此区间| G2
style G2 fill:#C44569,color:#fff,stroke-width:3px
style TARGET fill:#FF9F43,color:#000
```
```sql
-- 假设索引上有以下值: 10, 20, 30, 40, 50
-- 锁定 (20, 30) 这个开区间
SELECT * FROM t WHERE id = 25 FOR UPDATE;
-- id=25 不存在 → 不锁记录,锁住包含 25 的间隙 (20, 30)
-- InnoDB 在 B+ 树上查找 25:
-- → 左边最近的记录: 20(≤ 25 的最大值)
-- → 右边最近的记录: 30(> 25 的最小值)
-- → 间隙 = (20, 30),锁住它!
-- 其他事务不能在 (20, 30) 区间内插入任何值
```
@@ -385,12 +460,62 @@ flowchart TD
style R5 fill:#FF9F43,color:#000
```
> [!TIP] 实用记忆法
> [!TIP] 🧠 记忆卡片 1 — 锁类型三步判断法
>
> 看到一条 SQL,依次回答三个问题就能判断加锁类型:
> 1. **有索引吗?** → 无索引 = 全表锁(❌)
> 2. **是等值查询吗?** → 是 → 再看是否有唯一索引
> 3. **是范围查询吗?** → 是 → 每个命中记录都加 Next-Key Lock
>
> ```mermaid
> flowchart TD
> Start{"SQL 会加锁吗?<br/>(当前读?)"}
> Start -->|"普通 SELECT"| Snap["❌ 一致性读<br/>不加锁(MVCC)"]
> Start -->|"SELECT FOR UPDATE/SHARE<br/>UPDATE / DELETE / INSERT"| Q1{"1. 有索引吗?"}
>
> Q1 -->|"❌ 无索引"| Full["⚠️ 全表 Next-Key Lock<br/>退化为表锁"]
> Q1 -->|"✅ 有索引"| Q2{"2. 等值查询?"}
>
> Q2 -->|"是"| Q3{"3. 唯一索引?"}
> Q3 -->|"唯一 + 命中"| RL["📌 Record Lock"]
> Q3 -->|"唯一 + 未命中"| GL["🔒 Gap Lock"]
> Q3 -->|"非唯一"| NKL["🔑 Next-Key Lock"]
>
> Q2 -->|"范围查询"| NKL2["🔑 每条命中记录<br/>Next-Key Lock<br/>+ 最大值右侧 Gap"]
>
> style RL fill:#00B6BC,color:#fff
> style GL fill:#FF9F43,color:#000
> style NKL fill:#C44569,color:#fff
> style NKL2 fill:#C44569,color:#fff
> style Full fill:#EE5A24,color:#fff
> style Snap fill:#636e72,color:#fff
> ```
>
> **口诀:无索引全表炸,唯一命中只锁行,其余都带间隙锁。**
> [!TIP] 🧠 记忆卡片 2 — 锁兼容性速记
>
> | 已有 ↓ \ 请求 → | **S** | **X** |
> |---|---|---|
> | **S** | ✅ 共享读没问题 | ❌ 别人要写我得让 |
> | **X** | ❌ 我在写你别读 | ❌ 我在写你别碰 |
>
> **口诀:读读兼容,读写互斥,写写互斥。** 意向锁(IS/IX)之间永远不冲突——它们只是"挂牌子声明意图"。
> [!TIP] 🧠 记忆卡片 3 — RC vs RR 锁行为对比
>
> | | **RR(默认)** | **RC** |
> |---|---|---|
> | Gap Lock | ✅ 有(防幻读) | ❌ 无 |
> | Next-Key Lock | ✅ 默认算法 | 退化为 Record Lock |
> | 并发度 | 较低 | 较高 |
> | 死锁概率 | 较高(Gap Lock 多) | 较低 |
>
> **口诀:RC 没有间隙锁,所以不会防幻读,但并发更高、死锁更少。**
> [!TIP] 🧠 记忆卡片 4 — 死锁四板斧
>
> 1. **固定顺序** — 所有事务按同一顺序(如主键升序)访问资源
> 2. **一次锁完** — 减少持锁期间的等待窗口
> 3. **短事务** — 锁持有时间越短,冲突越少
> 4. **降级到 RC** — 砍掉 Gap Lock,死锁概率大降
## 死锁与排查