Files
autumn-recruitment/02.MySQL/transaction/事务隔离级别与锁机制.md

192 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [mysql, transaction-isolation, gap-lock, next-key-lock, deadlock-detection]
create time: 2026-08-08 18:00
update time: 2026-08-08 18:00
---
# 事务隔离级别与锁机制
## 概述
MySQL InnoDB 实现了四种标准事务隔离级别(READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE),每种级别对应不同的锁策略和并发冲突处理能力。理解行锁、间隙锁(Gap Lock)、临键锁(Next-Key Lock)的工作原理及其交互关系,是分析和解决死锁、幻读、并发更新冲突等问题的基础。
## 核心原理
### 行锁、间隙锁与临键锁的关系
InnoDB 的锁不是单一维度的,而是由多种锁类型组合而成:
| 锁类型 | 锁定范围 | 适用场景 |
|--------|---------|---------|
| Record Lock | 索引记录本身 | 单行精确锁定 |
| Gap Lock | 索引记录之间的"间隙",不含记录本身 | 防止其他事务在该间隙插入新记录 |
| Next-Key Lock | Record Lock + Gap Lock | 锁定区间(前开后闭),默认锁粒度 |
```mermaid
graph LR
subgraph "索引序列: 1, 5, 10, 15, 20"
G1["(-∞, 1)"] -->|"Gap Lock"| R1["1: Record Lock"]
R1 -->|"Gap Lock"| G2["(1, 5)"]
G2 -->|"Gap Lock"| R2["5: Record Lock"]
R2 -->|"Gap Lock"| G3["(5, 10)"]
G3 -->|"Gap Lock"| R3["10: Record Lock"]
R3 -->|"Gap Lock"| G4["(10, 15)"]
G4 -->|"Gap Lock"| R4["15: Record Lock"]
R4 -->|"Gap Lock"| G5["(15, +∞)"]
end
style G1 fill:#ffe0e0
style G2 fill:#ffe0e0
style G3 fill:#ffe0e0
style G4 fill:#ffe0e0
style G5 fill:#ffe0e0
style R1 fill:#e0ffe0
style R2 fill:#e0ffe0
style R3 fill:#e0ffe0
style R4 fill:#e0ffe0
```
**关键结论**:
- 默认情况下(RR 隔离级别),InnoDB 使用 Next-Key Lock,即 Record + Gap 的组合
- 如果使用唯一的索引(如主键)等值查询,Gap Lock 会被取消,只剩 Record Lock
- GC 隔离级别下,Gap Lock 被关闭(只加 Record Lock),这是它与 RR 的本质区别之一
### 锁的兼容矩阵
| 已有锁 ↓ \ 请求锁 → | Record R (共享锁) | Record X (排他锁) | Gap R | Gap X |
|---------------------|------------------|------------------|-------|-------|
| Record R | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ❌ 冲突 |
| Record X | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 |
| Gap R | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ❌ 冲突 |
| Gap X | ❌ 冲突 | ❌ 冲突 | ✅ 兼容* | ❌ 冲突 |
> [!NOTE] 共享锁与排他锁
> 共享锁(S Lock)允许多个事务同时持有,但与其他 S/X 锁都不兼容。排他锁(X Lock)独占资源,不与其他任何锁兼容。INSERT/UPDATE/DELETE 隐式加 X 锁,SELECT ... FOR UPDATE 显式加 X 锁,SELECT ... LOCK IN SHARE MODE 加 S 锁。
### 死锁检测机制
InnoDB 使用**等待图(Wait-For Graph)**进行死锁检测:
```mermaid
flowchart TD
subgraph "等待图示例"
T1["事务 T1"] -->|"等待"| L1["锁 L1"]
L1 -->|"持有者"| T2["事务 T2"]
T2 -->|"等待"| L2["锁 L2"]
L2 -->|"持有者"| T3["事务 T3"]
T3 -->|"等待"| L3["锁 L3"]
L3 -->|"持有者"| T1
end
CycleCheck{"检测到环<br/>T1→T2→T3→T1"}
CycleCheck -->|是| ChooseVictim["选择牺牲者<br/>回滚代价最小的事务"]
CycleCheck -->|否| ContinueWait["继续等待"]
ChooseVictim --> ReleaseDeadlock["释放死锁<br/>回滚牺牲者的事务"]
```
**死锁参数配置**:
| 参数 | 默认值 | 含义 |
|------|--------|------|
| `innodb_lock_wait_timeout` | 50 秒 | 等待锁的超时时间(不涉及死锁检测) |
| `innodb_deadlock_detect` | ON | 是否开启死锁检测 |
| `innodb_max_dirty_pages_pct` | 75% | 脏页比例阈值,超过时强制刷新 |
```
死锁处理流程:
1. 事务 A 申请锁 B → 被事务 B 持有 → 加入等待队列
2. 事务 B 申请锁 A → 被事务 A 持有 → 检测到环路
3. InnoDB 选择"回滚代价最小"的事务作为牺牲者
4. 回滚牺牲者的当前语句(非整个事务),释放其持有的锁
5. 另一事务继续执行
```
> [!WARNING] 牺牲者选择策略
> 回滚的是"当前语句"而非"整个事务"。如果语句包含多条 SQL,只回滚最后一条。这意味着事务可以继续重试后续语句,不会中断整个业务流程。
### Insert Intention Gap Lock
Insert Intention Gap Lock 是一种特殊的间隙锁,专门用于 INSERT 操作。它表示"我准备在这个间隙插入一条记录"。
```mermaid
graph LR
subgraph "索引值: 5 和 10"
R5["记录 5"] -->|"Gap (5,10)"| G1["间隙区"]
G1 -->|"Gap (5,10)"| R10["记录 10"]
end
T1["事务 T1: INSERT 值=7"] -->|"申请 Insert Intention<br/>Gap Lock on (5,10)"| G1
T2["事务 T2: INSERT 值=8"] -->|"申请 Insert Intention<br/>Gap Lock on (5,10)"| G1
G1 -->|"Insert Intention 锁互相兼容"| T2
```
两条 INSERT 语句即使目标间隙重叠,它们的 Insert Intention Gap Lock 也是**互相兼容**的。但如果其中一条已经是该间隙上的 Gap Lock(来自 UPDATE/DELETE),则新的 INSERT 会等待。
## 代码示例
### 场景分析:更新某区间内不存在记录时会加什么锁
```sql
-- 假设表中有记录: id IN (1, 5, 10, 15, 20)
-- 事务 A 执行:
UPDATE orders SET status = 1 WHERE id > 5 AND id < 15;
-- InnoDB 的行为分析:
-- 1. 扫描到 id=5 的记录 → Next-Key Lock: (5, 10](Record+Gap)
-- 2. 发现 id=10 的记录 → Record Lock: 10 本身(但 id=10 满足条件,被修改)
-- 3. 继续扫描,没有 id=11~14 的记录 → Next-Key Lock: (10, 15]
-- 4. 扫描到 id=15 → Record Lock: 15 本身(但不满足条件,不加锁)
-- 5. 最终锁定的范围:(5, 15],包括间隙和存在的记录
-- 此时另一个事务执行:
-- 事务 B: INSERT INTO orders (id, status) VALUES (12, 0);
-- 结果:被阻塞!因为 (10, 15) 间隙已被事务 A 锁定
```
```sql
-- 验证锁类型的 SQL 写法
-- 会话 1:开始事务并加锁
START TRANSACTION;
SELECT * FROM orders WHERE id = 10 FOR UPDATE;
-- 由于 id 是主键(唯一索引),只加 Record Lock 在 10 上
-- 会话 2:
SELECT * FROM orders WHERE id = 10 FOR UPDATE;
-- 阻塞!
SELECT * FROM orders WHERE id = 5 FOR UPDATE;
-- 不会被阻塞(5 ≠ 10,主键等值查询的 Gap Lock 已被去掉)
-- 如果是非唯一索引:
-- 索引 (status) 上有重复值,UPDATE ... WHERE status=1
-- 会对所有 status=1 的记录加 Record Lock + Gap Lock
```
## 实践场景
**场景一:排查生产环境死锁**
```sql
-- InnoDB 会打印死锁详情到错误日志
-- 也可以通过 performance_schema 查看
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
-- 分析死锁日志的步骤:
-- 1. 找到死锁发生的时间点和涉及的 SQL
-- 2. 画出等待图,确定循环等待链
-- 3. 按照推荐方案调整 SQL 执行顺序或加锁范围
```
**场景二:减少间隙锁的影响**
- 如果需要在高并发环境下批量插入数据,可以将隔离级别降到 RC
- RC 级别下 InnoDB 不使用 Gap Lock,INSERT 操作不会阻塞
- 但要注意:RC 不能解决幻读问题,需评估业务可接受度
**场景三:合理的索引设计降低锁竞争**
- 尽量使用唯一索引(主键)做精确更新,可以避免 Gap Lock
- 避免在大宽表的非唯一索引上做大范围 UPDATE/DELETE
- 大批量更新时分批次进行,缩短持有锁的时间
## 扩展阅读
- [[B+树索引原理]]
- [[ACID 与 MVCC 机制]]