vault backup: 2026-08-08 19:01:04

This commit is contained in:
2026-08-08 19:01:04 +08:00
parent 1a4a83bc67
commit e2975eb86b
46 changed files with 9370 additions and 0 deletions
@@ -0,0 +1,181 @@
---
tags: [mysql, acido-mvcc, undo-log, read-view, repeatable-read]
create time: 2026-08-08 18:00
update time: 2026-08-08 18:00
---
# ACID 与 MVCC 机制
## 概述
MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB 实现幻读隔离的核心机制。它通过维护数据的多个历史版本,使得读写操作互不阻塞,在保证事务隔离性的同时大幅提升并发性能。理解 MVCC 的实现细节——undo log、Read View、可见性判断——对于深入把握 MySQL 事务行为至关重要。
## 核心原理
### Undo Log 的结构
Undo Log 是 InnoDB 为了实现 MVCC 和事务回滚而维护的一种日志,它记录了事务修改前的旧数据版本。
```mermaid
graph TD
subgraph "Buffer Pool — 当前最新版本"
RowA["行记录 A<br/>version=3, TRX_ID=T5"]
end
subgraph "Undo Log Chain — 历史版本链"
Version3["版本3: val='updated_v3'<br/>TRX_ID=T5, next_undo=Version2"]
Version2["版本2: val='updated_v2'<br/>TRX_ID=T4, next_undo=Version1"]
Version1["版本1: val='original_val'<br/>TRX_ID=T3, next_undo=NULL"]
end
RowA -->|"DB_TRX_ID 指向"| Version3
Version3 --> Version2
Version2 --> Version1
```
Undo Log 的两类内容:
- **redo log**:物理日志,记录"在某处做了什么修改",用于崩溃恢复
- **undo log**:逻辑日志,记录"做了什么事情",用于回滚和 MVCC
每个行记录隐藏了两列:
- **DB_TRX_ID**:最近修改该行的事务 ID(6 字节)
- **DB_ROLL_PTR**:回滚指针,指向 undo log 中上一个版本的地址(7 字节)
> [!NOTE] Rollback Segment 的组织方式
> InnoDB 将 undo log 组织在 Rollback Segment 中,分为 undo log header 和 undo log record 两部分。每个事务的修改按时间顺序追加到 segment 尾部,形成版本号链。
### Read View 的生成规则
Read View 是 MVCC 的核心数据结构,定义了事务在某一时刻"能看到哪些版本"。
```mermaid
graph TD
subgraph "RC 隔离级别 - ReadView 生成时机"
RC_T1["事务开始"] --> RC_Q1["第一次查询时"]
RC_Q1 --> RC_ReadView["生成 ReadView"]
RC_ReadView --> RC_Q2["第二次查询时"]
RC_Q2 --> RC_NewRV["再次生成新的 ReadView"]
end
subgraph "RR 隔离级别 - ReadView 生成时机"
RR_T1["事务开始"] --> RR_FirstQ["第一次查询时"]
RR_FirstQ --> RR_ReadView["生成 ReadView"]
RR_ReadView --> RR_Q2["后续查询"]
RR_Q2 --> RR_SameRV["复用同一个 ReadView"]
end
style RC_ReadView fill:#ffeeaa
style RC_NewRV fill:#ffeeaa
style RR_ReadView fill:#99ffcc
style RR_SameRV fill:#99ffcc
```
**RC(Read Committed)和 RR(Repeatable Read)的关键区别**:
| 特性 | RC | RR |
|------|----|----|
| ReadView 生成时机 | 每条 SELECT 语句开始时生成 | 第一次查询时生成,后续复用 |
| 快照读可见性 | 每次查询都是最新提交的快照 | 全局一致视图,所有查询所见相同 |
| 解决幻读能力 | 不能解决 | 配合 Next-Key Lock 可以解决 |
**ReadView 的成员变量**:
- `m_ids`:生成 ReadView 时当前活跃的事务 ID 列表
- `m_low_limit_id`:最小的活跃事务 ID(下一个将要分配的事务 ID)
- `m_up_limit_id`:最大活跃事务 ID + 1
- `m_trx_id_level`:活跃事务的最小 trx_id
### 可见性判断流程
这是 MVCC 最核心的算法:给定一个行版本和一个 ReadView,判断当前事务是否能看到这个版本。
```mermaid
flowchart TD
Start["开始: 行版本 trx_id + ReadView"] --> Compare1{"trx_id < m_up_limit_id?"}
Compare1 -->|否| CheckActive{"trx_id ∈ m_ids?"}
Compare1 -->|是| Visible["✓ 可见"]
CheckActive -->|否| Visible
CheckActive -->|是| Compare2{"trx_id < m_low_limit_id?"}
Compare2 -->|是| Invisible["✗ 不可见<br/>事务尚未提交"]
Compare2 -->|否| CheckOwn{"trx_id = 当前事务ID?"}
CheckOwn -->|是| VisibleSelf["✓ 可见<br/>自己修改的版本"]
CheckOwn -->|否| Compare3{"trx_id 已提交?"}
Compare3 -->|是| Visible
Compare3 -->|否| Invisible
style Visible fill:#90ee90
style VisibleSelf fill:#90ee90
style Invisible fill:#ff9999
```
**简化记忆口诀**:
1. **小于 up_limit_id** → 肯定可见(生成 ReadView 前就提交了)
2. **大于等于 low_limit_id** → 肯定不可见(生成 ReadView 后才启动的)
3. **介于两者之间** → 再看是否在活跃列表中,以及是不是自己
### Next-Key 与 ReadView 的配合
RR 隔离级别下,InnoDB 用"MVCC + Next-Key Lock"双管齐下来解决幻读:
- **快照读**(普通 SELECT)靠 MVCC/ReadView 解决
- **当前读**(SELECT ... FOR UPDATE / LOCK IN SHARE MODE / UPDATE / DELETE)靠 Next-Key Lock 解决
```sql
-- 快照读:不会锁住区间,依赖 ReadView 保证一致性
SELECT * FROM orders WHERE status = 1;
-- 当前读:加锁,防止其他事务插入或修改
SELECT * FROM orders WHERE status = 1 FOR UPDATE;
```
## 代码示例
```sql
-- 演示 RC vs RR 在读已提交数据时的差异
-- 会话 1:设置隔离级别为 RR(默认)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
-- 此时 balance = 1000
-- 会话 2:修改并提交
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
UPDATE accounts SET balance = 2000 WHERE id = 1;
COMMIT;
-- 会话 1:再次查询
SELECT balance FROM accounts WHERE id = 1;
-- RR: 仍然返回 1000(复用了初始 ReadView)
-- 如果换成 RC: 返回 2000(重新生成 ReadView)
```
## 实践场景
**场景一:理解为什么 RR 下"看不到别人刚提交的数据"**
- 在 RR 中,事务内的所有快照读看到的是同一个一致性视图
- 如果你需要在事务中看到其他事务的最新提交结果,可以在适当时候设置 `SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;`(需单独事务生效)或在应用层做补偿查询
**场景二:监控 undo log 空间**
```sql
-- 查看 undo log 使用情况
SELECT * FROM sys.innodb_old_tablespaces;
-- 长时间运行的未提交事务会产生大量 undo 记录
-- 影响 buffer pool 的内存占用
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id
FROM information_schema.innodb_trx
WHERE trx_state = 'RUNNING' AND TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;
```
**场景三:避免长事务导致的 undo 膨胀**
- 不要在事务中进行大量无关操作
- 批量更新拆分成小批次事务
- 定时清理长时间运行的事务:kill 异常休眠的线程
## 扩展阅读
- [[B+树索引原理]]
- [[事务隔离级别与锁机制]]
@@ -0,0 +1,191 @@
---
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 机制]]