From 9fc46eac249671d42241a88bc4bb25685bec0825 Mon Sep 17 00:00:00 2001
From: hhs <386998068@qq.com>
Date: Thu, 21 May 2026 20:00:46 +0800
Subject: [PATCH] vault backup: 2026-05-21 20:00:46
---
hhs/MySQL/06-事务与并发控制/25-MVCC 原理.md | 2 +-
hhs/MySQL/06-事务与并发控制/26-锁机制总览.md | 117 ++++++++++++++++---
2 files changed, 100 insertions(+), 19 deletions(-)
diff --git a/hhs/MySQL/06-事务与并发控制/25-MVCC 原理.md b/hhs/MySQL/06-事务与并发控制/25-MVCC 原理.md
index aa8f51e..cf06729 100644
--- a/hhs/MySQL/06-事务与并发控制/25-MVCC 原理.md
+++ b/hhs/MySQL/06-事务与并发控制/25-MVCC 原理.md
@@ -258,8 +258,8 @@ sequenceDiagram
alt 可见
UL-->>T: 返回该版本
else 仍不可见
- UL->>UL: 继续往前追溯
loop 直到找到可见版本或链表结束
+ UL->>UL: 继续往前追溯
end
end
end
diff --git a/hhs/MySQL/06-事务与并发控制/26-锁机制总览.md b/hhs/MySQL/06-事务与并发控制/26-锁机制总览.md
index 39c9b91..1411e48 100644
--- a/hhs/MySQL/06-事务与并发控制/26-锁机制总览.md
+++ b/hhs/MySQL/06-事务与并发控制/26-锁机制总览.md
@@ -7,10 +7,22 @@ create time: 2026-05-16 00:00
## 概述
-InnoDB 的锁体系是分层次的——从全局到表级到行级,每种锁粒度不同、适用场景不同。理解锁的分类和使用时机是排查锁冲突和设计高并发系统的核心。
+想象一个大型图书馆:**全局锁**相当于闭馆——所有人只能看不能借;**表锁**相当于某个楼层封闭维护;**行锁**则精确到某一本书被借走,其他人想看同一本得排队。
+
+InnoDB 的锁体系就是这样的分层结构——从全局到表级到行级,**粒度越细,并发能力越强,但管理成本也越高**。理解锁的分类和使用时机,是排查锁冲突和设计高并发系统的核心。
+
+> [!QUESTION] 在往下读之前,先想一个问题
+> 如果两个用户同时下单买同一件商品(库存只剩 1 件),数据库怎么保证不超卖?
+> 带着这个问题往下读,你会在「行级锁」和「当前读」部分找到答案。
## 锁分类全图
+MySQL 的锁种类看似很多,但本质上就是**两个维度**的组合:
+- **按粒度**:锁的范围有多大?(全局 → 表 → 行)
+- **按行为**:锁允许别人做什么?(共享读 vs 独占写)
+
+下面这张图把所有锁按层次关系展开:
+
```mermaid
graph BT
subgraph "按粒度层次"
@@ -48,6 +60,8 @@ graph BT
## 全局锁
+全局锁是最"暴力"的锁——锁住整个数据库实例,所有线程只能读不能写。就像商场打烊后,所有店铺都暂停营业。
+
```sql
-- 对整个数据库加锁,不允许任何写入
FLUSH TABLES WITH READ LOCK;
@@ -56,7 +70,7 @@ FLUSH TABLES WITH READ LOCK;
UNLOCK TABLES;
```
-**典型场景**:逻辑备份(mysqldump)时确保数据一致,期间不允许任何写操作。
+**典型场景**:逻辑备份(mysqldump)时确保数据一致,期间不允许任何写操作。生产环境的数据库通常有几十 GB 甚至更大,如果备份过程中有人在写数据,备份出来的数据就会"前后矛盾"——所以需要全局锁来保证快照一致性。
> [!QUESTION] ❓ 思考:如果备份期间有人在做 DDL 怎么办?
>
@@ -70,7 +84,10 @@ mysqldump --single-transaction --routines db_name > backup.sql
## 元数据锁(MDL, Metadata Lock)
-MDL 是自动管理的,用户无法手动控制。目的是防止「一边有人查表结构,另一边有人在改表结构」。
+MDL 是**自动管理**的,用户无法手动控制——你甚至感知不到它的存在。它的目的很简单:防止「一边有人查数据,另一边有人在改表结构」。
+
+> [!QUESTION] 为什么需要这种保护?
+> 想象一下:事务 A 正在查 `users` 表的 `name` 字段,此时事务 B 执行 `ALTER TABLE users DROP COLUMN name`。如果 InnoDB 不加保护,事务 A 读到一半发现字段没了——这不是 Bug,这是灾难。MDL 就是为了避免这种"读写冲突"。
> [!QUESTION] ❓ 为什么 MySQL 5.5+ 才引入 MDL?
>
@@ -96,7 +113,9 @@ ALTER TABLE users ADD COLUMN bio TEXT;
## 表级锁
-### 自研工具锁
+表级锁就是把整张表锁住——粒度比行锁粗,但开销小、加锁快。
+
+### 显式表锁(LOCK TABLES)
```sql
LOCK TABLES users WRITE, orders READ;
@@ -106,13 +125,26 @@ LOCK TABLES users WRITE, orders READ;
UNLOCK TABLES; -- 显式释放
```
+> [!NOTE] 实际开发中很少用到
+> 显式表锁在 InnoDB 项目里几乎没有使用场景——InnoDB 的行锁已经足够精细。你会在一些老的 MyISAM 项目或特殊的数据迁移脚本中见到它。
+
### MyISAM 的自动表锁
-MyISAM 在执行每条语句前自动申请表锁,执行完毕后自动释放。InnoDB 则几乎不使用表锁——除了某些 DDL 操作。
+MyISAM 引擎的锁策略非常"粗犷":**执行每条语句前自动申请表锁,执行完毕后自动释放**。这意味着同一张表,同时只能有一个写操作——并发能力很差。这也是为什么 InnoDB 取代 MyISAM 成为默认引擎的核心原因之一。
+
+> [!QUESTION] 💡 为什么 InnoDB "几乎不用"表锁?
+> 因为 InnoDB 支持行级锁,锁粒度可以从"整张表"精确到"某一行"。行锁的并发能力远超表锁——就像一个会议室,表锁是"整个会议室一次只能进一组人",行锁是"每个座位可以分配给不同组"。InnoDB 只在执行 DDL(如 `ALTER TABLE`)时才会用到表锁。
## 行级锁 — 核心中的核心
-InnoDB 的行锁都是**加在索引 record 上**的。没有索引 = 退化为表锁。
+终于来到最重要的部分了。行级锁是 InnoDB 的看家本领——锁的范围精确到单条记录,并发能力最强。
+
+> [!IMPORTANT] 核心前提:行锁是加在索引上的!
+> InnoDB 的行锁**不是锁在数据行上,而是锁在索引记录上**。这意味着:
+> - 如果你的 `WHERE` 条件走索引 → 只锁匹配的行
+> - 如果 `WHERE` 条件**没有索引** → MySQL 扫描全表索引 → **退化为表锁**!
+>
+> 这是线上最常踩的坑之一,很多慢查询事故的根源就在于此。
> [!WARNING] ⚠️ 面试高频坑点:无索引 UPDATE
>
@@ -126,11 +158,16 @@ InnoDB 的行锁都是**加在索引 record 上**的。没有索引 = 退化为
### 三种基本锁类型
-| 名称 | 符号 | 兼容 | 说明 |
-|------|------|------|------|
-| **共享锁(S 锁)** | `LOCK IN SHARE MODE` | S+S ✅, S+X ❌ | 读锁,多个事务可同时持有 |
-| **排他锁(X 锁)** | `FOR UPDATE` | X+X ❌ | 写锁,独占 |
-| **意向锁(IS/IX)** | 自动加 | 用于表级兼容检测 | 表明事务想在某行上加 S/X |
+理解行锁,先从三种最基础的锁开始。用一个类比:**S 锁像"图书馆里多人共读一本书",X 锁像"只有你能翻这本书"**。
+
+| 名称 | 符号 | 兼容性 | 通俗解释 |
+|------|------|--------|----------|
+| **共享锁(S 锁)** | `LOCK IN SHARE MODE` | S+S ✅, S+X ❌ | "我只读,你也可以读,但别改" |
+| **排他锁(X 锁)** | `FOR UPDATE` | X+X ❌ | "我要改这行,你们都等着" |
+| **意向锁(IS/IX)** | 自动加 | 用于表级兼容检测 | "我打算锁表里的某行" — 纯粹的效率优化 |
+
+> [!QUESTION] 为什么需要意向锁?
+> 如果没有意向锁,当有人想给整张表加表锁时,需要**逐行扫描**才能知道有没有行锁存在——这对于百万行的表来说代价太高了。意向锁的作用就是:在表级做一个"标记",告诉后来者"这张表里有人在锁行",从而**快速判断兼容性,不用逐行扫描**。
```sql
-- 显式加共享锁
@@ -167,11 +204,18 @@ flowchart TD
## 记录锁、间隙锁、临键锁
-这是 InnoDB 最精妙的设计,也是理解并发控制的关键。
+这是 InnoDB 最精妙的设计,也是面试中的高频考点。三种锁解决的问题各不相同:
+
+- **Record Lock**:锁住已存在的记录 → 防止并发修改
+- **Gap Lock**:锁住记录之间的"空隙" → 防止并发插入
+- **Next-Key Lock**:前两者的组合 → RR 隔离级别下防幻读的终极武器
+
+> [!NOTE] 为什么需要锁"空隙"?
+> 在 RR 隔离级别下,同一个事务内两次范围查询应该返回相同的结果(防止幻读)。如果只锁记录不锁间隙,其他事务就能在间隙里插入新行,导致第二次查询多出"幽灵记录"。Gap Lock 就是为了堵住这个漏洞。
### Record Lock(记录锁)
-锁住**具体的索引记录**。
+锁住**具体的索引记录**——最简单直观的锁类型,就像在超市货架上贴一个"已有人要买"的标签。
```sql
-- idx_status 上有唯一索引
@@ -182,7 +226,10 @@ SELECT * FROM orders WHERE status = 'pending' FOR UPDATE;
### Gap Lock(间隙锁)
-锁住**索引记录之间的间隙**,不包含记录本身。目的是阻止其他事务在间隙中插入新记录。
+锁住**索引记录之间的间隙**,不包含记录本身。目的只有一个:**阻止其他事务在这个间隙中插入新记录**。
+
+> [!QUESTION] Gap Lock 和 Record Lock 最大的区别是什么?
+> Record Lock 锁的是"已经存在的东西",Gap Lock 锁的是"还不存在但可能出现的东西"。一个是保护现在,一个是防止未来。
```sql
-- 假设索引上有以下值: 10, 20, 30, 40, 50
@@ -195,7 +242,9 @@ SELECT * FROM t WHERE id = 25 FOR UPDATE;
### Next-Key Lock = Record Lock + Gap Lock
-**左开右闭区间** `(prev_value, value]`,是 RR 模式下默认的锁算法。
+Next-Key Lock 是 InnoDB 在 **RR(可重复读)** 隔离级别下的**默认行锁算法**。它锁住的是一个**左开右闭区间** `(prev_value, value]`——既锁住当前记录,也锁住左边的间隙。
+
+**一句话总结:Record Lock + Gap Lock = Next-Key Lock,既能防并发修改,又能防并发插入。**
### 一图理解 Next-Key Lock
@@ -288,7 +337,11 @@ sequenceDiagram
### ⚡ 当前读 vs 一致性读:谁触发了锁?
-理解这一组概念是掌握锁机制的最终一步——**并不是所有 SELECT 都会加锁**。
+理解这一组概念是掌握锁机制的最终一步——**并不是所有 SELECT 都会加锁**。很多 Bug 和死锁排查,都源于对这两种读模式的混淆。
+
+> [!QUESTION] 一个关键直觉
+> - **一致性读(快照读)**= "我只看历史照片" → 不需要加锁,因为看的是过去的数据快照
+> - **当前读**= "我要看最新现场" → 必须加锁,因为要保证看到的是"此刻最新的数据",不能让别人在我看的瞬间改掉它
| 读取方式 | SQL 示例 | 是否加锁 | 数据来源 |
|---------|---------|---------|---------|
@@ -323,7 +376,7 @@ flowchart TD
Q -->|"精确等值(unique index)"| R1["Record Lock
仅锁那条记录"]
Q -->|"精确等值(non-unique index)"| R2["Next-Key Lock
记录 + 左侧间隙"]
- Q -->|"范围查询"
WHERE id > 10"> R3["每个匹配记录的
Next-Key Lock + 最大值右侧间隙"]
+ Q -->|"范围查询
WHERE id > 10"| R3["每个匹配记录的
Next-Key Lock + 最大值右侧间隙"]
Q -->|"无索引条件"| R4["全表记录的 Next-Key Lock
退化为表锁效果 ⚠️"]
Q -->|"DELETE | UPDATE | INSERT"| R5["INSERT: Gap Lock on gap
DELETE/UPDATE: Record Lock"]
@@ -341,7 +394,14 @@ flowchart TD
## 死锁与排查
-在生产环境中,死锁不是「会不会发生」的问题,而是「什么时候发生」。理解死锁的形成过程比避免死锁更重要。
+在生产环境中,死锁不是「会不会发生」的问题,而是「什么时候发生」。
+
+> [!NOTE] 什么是死锁?一个生活类比
+> 两个人在狭窄的走廊迎面走来,都往左让→都往右让→又都往左……谁也过不去,这就是死锁。在数据库里,就是**两个事务互相等待对方释放锁,形成了一个环**。
+>
+> MySQL InnoDB 会自动检测死锁(通过等待图算法),并**回滚代价较小的那个事务**,让另一个继续执行。所以死锁本身不会导致数据库崩溃,但会导致业务请求失败。
+
+理解死锁的形成过程比避免死锁更重要:
```sql
-- 查看最近的死锁信息
@@ -384,6 +444,27 @@ sequenceDiagram
> 3. **短事务原则**:尽可能少地参与竞争
> 4. **降低隔离级别**:RC 模式不使用 Gap Lock,大幅降低死锁概率
+## 总结:一张图记住锁机制的核心逻辑
+
+回顾开篇的问题:两个用户同时买同一件商品,数据库怎么保证不超卖?
+
+答案是:
+1. `UPDATE inventory SET stock = stock - 1 WHERE product_id = 100` — 这是**当前读**,自动加 X 锁
+2. 因为 `product_id` 是索引,InnoDB 用 **Record Lock** 锁住这一行
+3. 第二个事务到达时发现行被锁住了,**等待**第一个事务提交或回滚
+4. 第一个事务提交后,第二个事务拿到锁,此时 `stock` 已经是 0,`stock - 1 = -1` 由业务层判断拦截
+
+> [!TIP] 面试速记
+>
+> | 问题 | 答案 |
+> |------|------|
+> | InnoDB 有哪些锁? | 全局锁、MDL、表锁、行锁(S/X/IS/IX/Record/Gap/Next-Key) |
+> | 行锁加在哪? | 索引记录上,不是数据行上 |
+> | 无索引会怎样? | 行锁退化为表锁 |
+> | 什么是 Next-Key Lock? | Record Lock + Gap Lock,左开右闭区间 |
+> | RC 和 RR 的锁区别? | RC 没有 Gap Lock,RR 有 |
+> | 什么时候不加锁? | 一致性读(快照读),靠 MVCC |
+
## 关联笔记
- [[hhs/MySQL/06-事务与并发控制/23-ACID 与原子性实现]] — Redo Log 和 Undo Log 的基础知识