vault backup: 2026-05-21 22:11:56
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user