vault backup: 2026-05-21 20:00:46
This commit is contained in:
@@ -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<br/>仅锁那条记录"]
|
||||
Q -->|"精确等值(non-unique index)"| R2["Next-Key Lock<br/>记录 + 左侧间隙"]
|
||||
Q -->|"范围查询"<br/>WHERE id > 10"> R3["每个匹配记录的<br/>Next-Key Lock + 最大值右侧间隙"]
|
||||
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"]
|
||||
|
||||
@@ -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 的基础知识
|
||||
|
||||
Reference in New Issue
Block a user