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
@@ -84,6 +84,32 @@ flowchart LR
2. **在新行上记录前一个版本的回滚指针** (`DB_ROLL_PTR`)
3. 更新 `DB_TRX_ID` 为当前事务 ID
### 具体示例:一行数据经历三次 UPDATE
```sql
-- 初始: INSERT INTO product(id, name, price) VALUES(1, '键盘', 200);
-- 行记录: {id=1, name='键盘', price=200, DB_TRX_ID=50, DB_ROLL_PTR=NULL}
```
随后三个事务依次修改 `price`:
| 操作 | 聚簇索引中的行 | undo log 新增节点 |
|------|---------------|-----------------|
| 初始 INSERT | `{price=200, trx=50, roll_ptr=NULL}` | — |
| TXN 60: `SET price=180` | `{price=180, trx=60, roll_ptr→undo_1}` | undo_1: `{price=200, trx=50}` |
| TXN 70: `SET price=150` | `{price=150, trx=70, roll_ptr→undo_2}` | undo_2: `{price=180, trx=60}` |
| TXN 80: `SET price=99` | `{price=99, trx=80, roll_ptr→undo_3}` | undo_3: `{price=150, trx=70}` |
此时 version chain 为:
```
聚簇索引行(price=99) ──roll_ptr──→ undo_3(price=150) ──→ undo_2(price=180) ──→ undo_1(price=200)
trx=80 trx=70 trx=60 trx=50
```
> [!TIP] 类比理解
> 把 version chain 想象成「编辑历史」——每次修改都不会覆盖原文,而是把旧版本存进回收站(undo log),新版本放回桌面(聚簇索引)。一致性读就是根据你打开文档的时间点,去回收站翻找对应的版本。
> [!QUESTION] 为什么是链表而不是数组?
> 因为版本数量在写入时无法预知。链表允许 O(1) 追加新版本,且通过 `DB_ROLL_PTR` 可以高效遍历——只沿着真正需要回溯的路径走。
@@ -109,43 +135,43 @@ flowchart TD
style I fill:#FF6B6B,color:#fff
```
> [!NOTE] 可见性判断规则
> - 若 `trx_id == ReadView.m_ids` 中的某个值 → **当前事务自己改的,可见**
> - 若 `trx_id < min_trx_id` → **提交于快照之前的,可见**
> - 若 `trx_id >= max_trx_id` → **提交于快照之后的,不可见**
> - 若在 `[min_trx_id, max_trx_id)` 之间且在 `m_ids` 列表中 → **可见(该事务在 ReadView 创建时未提交)**
> - 若在 `[min_trx_id, max_trx_id)` 之间但**不在** `m_ids` 列表中 → **不可见(该事务已提交)**
> [!NOTE] 可见性判断规则(5 条,从上到下依次判断)
> 1. `trx_id == 当前事务 ID` → ✅ **自己改的,可见**
> 2. `trx_id < min_trx_id` → ✅ **快照前已提交,可见**
> 3. `trx_id >= max_trx_id` → ❌ **快照后才启动,不可见**
> 4. `trx_id ∈ [min_trx_id, max_trx_id)` 且**在 `m_ids` 中** → ❌ **快照时尚未提交,不可见**
> 5. `trx_id ∈ [min_trx_id, max_trx_id)` 且**不在 `m_ids` 中** → ✅ **快照前已提交,可见**
>
> > 核心直觉:`m_ids` 就是「快照瞬间还在跑的事务名单」,名单上的事务其修改对当前快照**不可见**。
### 一致性读的完整流程
```mermaid
flowchart TD
A["普通 SELECT"] --> B{"目标行是否有锁?"}
B -->|无锁| C["直接读取当前行"]
B -->|有锁| D["跳过当前行"]
D --> E{"DB_ROLL_PTR 是否存在?"}
E -->|否| F["无可视版本, 返回空"]
E -->|是| G["跟随指针读取 undo log 旧版本"]
A["普通 SELECT"] --> B["读取当前行最新版本"]
B --> C{"用 ReadView 判断<br/>DB_TRX_ID 是否可见?"}
C -->|可见| D["✅ 返回此版本"]
C -->|不可见| E{"DB_ROLL_PTR 是否存在?"}
E -->|否| F["❌ 无可视版本, 返回空"]
E -->|是| G["沿 DB_ROLL_PTR 读取 undo log 旧版本"]
G --> H{"旧版本 trx_id 可见?"}
H -->|是| I["✅ 返回可见版本"]
H -->|否| J{"还有更早版本?"}
J -->|是| G
J -->|否| F
C --> K["返回结果"]
I --> K
F --> K
style B fill:#FF9F43,color:#000
style I fill:#00D866,color:#fff
H -->|可见| D
H -->|不可见| I{"还有更早版本?"}
I -->|是| G
I -->|否| F
style C fill:#FF9F43,color:#000
style D fill:#00D866,color:#fff
style F fill:#FF6B6B,color:#fff
```
> [!NOTE] 与之前版本的区别
> 一致性读**不关心行上有没有锁**——它总是从最新版本开始,通过 ReadView 的可见性规则决定该版本是否对当前事务可见。不可见就沿 undo chain 往回找,直到找到第一个可见版本(或链尾)。
> [!TIP] 一致性读的性能优势
> 由于不需要加锁、不需要等待锁释放,一致性读在大量只读场景下几乎不产生并发开销。这就是为什么报表查询、数据导出都应该用普通 `SELECT`——既不影响业务写入,也避免了自己被阻塞。
@@ -154,6 +180,66 @@ flowchart TD
> [!NOTE] 为什么叫「快照读」?
> 因为它读的不是磁盘上的最新数据,而是某个时间点的数据「快照」。在 RR 模式下,这个快照在事务第一次 SELECT 时就冻结了;在 RC 模式下,每次 SELECT 都刷新快照。
### 端到端示例:跟着 ReadView 走一遍
> [!QUESTION] 先想想:如果三个事务交替修改同一行,第四个事务来做一致性读,它到底看到谁的版本?
假设有一行 `account(id=1, balance=1000)`,当前 `max_trx_id = 104`:
| 时间 | 事务 | 操作 | 状态 |
|------|------|------|------|
| T1 | TXN 101 | `UPDATE balance=900` | 已提交 ✅ |
| T2 | TXN 102 | `UPDATE balance=800` | 未提交 🔒 |
| T3 | TXN 103 | `UPDATE balance=700` | 已提交 ✅ |
| T4 | **TXN 104(我们)** | `BEGIN; SELECT balance` | 开始一致性读 |
此时 InnoDB 构建 ReadView:
```
m_ids = [102] ← 快照时仍在运行的事务(只有 TXN 102 未提交)
min_trx_id = 102
max_trx_id = 105 ← 下一个待分配的事务 ID
```
行的 version chain(从新到旧):
```
balance=700 (trx=103) ← balance=800 (trx=102) ← balance=900 (trx=101) ← balance=1000 (trx=旧)
```
一致性读沿 version chain 逐版本判断:
1. **trx=103**:在 `[102,105)` 内且**不在** m_ids 中 → 已提交 → ✅ 可见
2. 直接返回 `balance=700`,不再往后找
> [!TIP] 关键洞察
> 虽然最新版本是 `balance=700`(trx=103),但如果你把 TXN 103 提交撤销、让 trx=102 先提交,那我们就会跳过 trx=102 的版本(它在 m_ids 中不可见),最终看到 trx=101 的 `balance=900`。可见性判断完全取决于「谁在快照瞬间已经提交」。
### RR vs RC 的快照时机差异
```mermaid
sequenceDiagram
participant T as TXN A (RR 模式)
participant T2 as TXN B
participant DB as InnoDB
Note over T: BEGIN
T->>DB: SELECT balance FROM account WHERE id=1
Note over T,DB: 快照创建 (ReadView #1)
T2->>DB: UPDATE balance=500 WHERE id=1
T2->>DB: COMMIT
T->>DB: SELECT balance FROM account WHERE id=1
Note over T,DB: 复用 ReadView #1 → 看不到 500
T->>DB: SELECT balance FROM account WHERE id=1
Note over T,DB: 仍然复用 ReadView #1 → 还是看不到 500
Note over T: 整个事务期间看到的值始终一致
```
如果 TXN A 用的是 **RC 模式**,每次 SELECT 都会创建新的 ReadView,第二次 SELECT 就能看到 `balance=500`。
## 深入理解当前读
```sql