vault backup: 2026-05-21 19:31:28

This commit is contained in:
hhs
2026-05-21 19:31:28 +08:00
parent 531d4b7d8c
commit f9d21f0026
9 changed files with 1290 additions and 78 deletions
@@ -142,7 +142,7 @@ sequenceDiagram
participant BGL as Binlog
participant RDL as Redo Log
App->>S: BEGIN; UPDATE ... COMMIT;
App->>S: BEGIN → UPDATE → COMMIT
S->>IB: Prepare 事务
IB->>RDL: 写入 Redo Log (prepare 状态)
@@ -124,25 +124,30 @@ sequenceDiagram
participant B as Transaction B
A->>RC_DB: BEGIN;
A->>RC_DB: SELECT * FROM t; -- Read View #1
A->>RC_DB: SELECT * FROM t; -- Read View #2 (new each time)
A->>RC_DB: SELECT * FROM t
Note over A,RC_DB: Read View #1
A->>RC_DB: SELECT * FROM t
Note over A,RC_DB: Read View #2 (new each time)
B->>RC_DB: BEGIN;
B->>RC_DB: INSERT INTO t VALUES (...);
B->>RC_DB: INSERT INTO t VALUES (...)
B->>RC_DB: COMMIT;
A->>RC_DB: SELECT * FROM t; -- Read View #3
A->>RC_DB: SELECT * FROM t
Note over A,RC_DB: Read View #3
Note over RC_DB: Can see new rows committed by B!
A->>RR_DB: BEGIN;
A->>RR_DB: SELECT * FROM t; -- Read View #1
A->>RR_DB: SELECT * FROM t; -- Reuses Read View #1
A->>RR_DB: SELECT * FROM t
Note over A,RR_DB: Read View #1
A->>RR_DB: SELECT * FROM t
Note over A,RR_DB: Reuses Read View #1
B->>RR_DB: BEGIN;
B->>RR_DB: INSERT INTO t VALUES (...);
B->>RR_DB: INSERT INTO t VALUES (...)
B->>RR_DB: COMMIT;
A->>RR_DB: SELECT * FROM t;
A->>RR_DB: SELECT * FROM t
Note over RR_DB: Cannot see B's rows - RV#1 was created before B committed
```
@@ -12,16 +12,17 @@ MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB
> [!TIP] 一句话理解 MVCC
> MVCC 的本质是让每个事务看到「一个时间点的快照」,而不是磁盘上实时的数据。就像给数据库拍照——你在照片里看到的是拍那一瞬间的样子,之后别人怎么改都不会影响你。
## 背后的三个支撑字段
## 行记录的隐藏字段
每行记录额外隐藏了两个字段:
每行记录在用户定义的列之外,InnoDB 还悄悄塞了两个隐藏字段:
| 字段 | 类型 | 作用 |
|------|------|------|
| **DB_TRX_ID** | 6 bytes | 最近修改该行的事务 ID |
| **DB_ROLL_PTR** | 7 bytes | 回滚指针,指向对应的 Undo Log |
此外,二级索引的叶子节点还保存了**主键值**(而不是完整数据),这也是 MVCC 能够工作的基础。
> [!NOTE] 二级索引与 MVCC 的关系
> 二级索引的叶子节点不存储完整行数据,只保存**主键值**。因此通过二级索引查数据时,必须「回表」到聚簇索引才能拿到完整的行记录(包括 DB_TRX_ID)。这意味着 MVCC 的可见性判断始终发生在聚簇索引层面。
```mermaid
flowchart LR
@@ -43,6 +44,9 @@ flowchart LR
## Version Chain(版本链)
> [!TIP] 用 Git 理解版本链
> 版本链就像 Git 的提交历史——每次 `UPDATE` 相当于一次 `git commit`,旧版本不会被覆盖,而是通过 `DB_ROLL_PTR`(类似 `parent commit` 指针)串联起来。你想看某个历史版本?沿着指针往前「checkout」就行了。
当一行被 UPDATE 时,InnoDB 不会直接修改原数据,而是:
1. 在 Undo Log 中保留旧版本
@@ -111,7 +115,7 @@ flowchart TD
```
> [!NOTE] min_trx_id / max_trx_id 的作用区间
> 这两个字段一起定义了一个半开区间 `[min_trx_id, max_trx_id)`。trx_id 落在这个区间之外的,可以直接通过两条边界判断得出结论;只有落在这个区间的,才需要进一步检查是否在 m_ids 中。这样可以减少不必要的列表扫描。
> 这两个字段一起定义了一个半开区间 `[min_trx_id, max_trx_id)`。trx_id 落在这个区间之外的,可以直接通过两条边界判断得出结论——小于 min 的一定可见,大于等于 max 的一定不可见;只有落在这个区间的,才需要进一步检查是否在 m_ids 中。这样可以减少不必要的列表扫描。
## 可见性判断规则
@@ -119,36 +123,48 @@ flowchart TD
```mermaid
flowchart TD
A["行记录的 trx_id"] --> B{"trx_id < min_trx_id?"}
B -->|是| V1["✅ 可见\n事务在 ReadView 前已提交"]
A["行记录的 trx_id"] --> S{"trx_id == creator_trx_id?"}
S -->|是| V0["✅ 可见\n我自己改的,当然看得到"]
S -->|否| B{"trx_id < min_trx_id?"}
B -->|是| V1["✅ 可见\n在 ReadView 创建前已提交"]
B -->|否| C{"trx_id >= max_trx_id?"}
C -->|是| V2["✅ 可见\n事务在 ReadView 后才开始"]
C -->|是| V2["❌ 不可见\n在 ReadView 创建后才启动"]
C -->|否| D{"trx_id 在 m_ids 中?"}
D -->|否| V3["✅ 可见\n不在活跃列表 → 已提交"]
D -->|是| V4["❌ 不可见\n创建时还在运行"]
D -->|是| V4["❌ 不可见\n创建 ReadView 时还在运行"]
style V0 fill:#00B6BC,color:#fff
style V1 fill:#00D866,color:#fff
style V2 fill:#00D866,color:#fff
style V2 fill:#EE5A24,color:#fff
style V3 fill:#00D866,color:#fff
style V4 fill:#EE5A24,color:#fff
```
### 四个规则的速记口诀
### 五个规则的速记口诀
| 规则 | 条件 | 结论 | 通俗解释 |
|------|------|------|---------|
| **早提交** | `trx_id < min_trx_id` | ✅ 可见 | 比你早起的人已经下班走了 → 你能看到 |
| **晚启动** | `trx_id >= max_trx_id` | ✅ 可见 | 比你晚来的人还没到办公室 → 你看不到他写的东西 |
| **已退出** | `trx_id ∉ m_ids` | ✅ 可见 | 名单上没有此人 → 他已经提交了 |
| **正活跃** | `trx_id ∈ m_ids` | ❌ 不可见 | 正在工位上坐着呢 → 别看他未完成的产出 |
| **自己改的** | `trx_id == creator_trx_id` | ✅ 可见 | 自己写的代码自己当然能看见 |
| **早提交** | `trx_id < min_trx_id` | ✅ 可见 | 比你早到的人已经提交下班了 → 你能看到他的产出 |
| **晚启动** | `trx_id >= max_trx_id` | ❌ 不可见 | 比你晚到的人还在加班 → 他的产出你还没资格看 |
| **已退出** | `trx_id ∉ m_ids` | ✅ 可见 | 名单上没有此人 → 说明他已经提交退出了 |
| **正活跃** | `trx_id ∈ m_ids` | ❌ 不可见 | 正在工位上坐着呢 → 别看他未完成的半成品 |
> [!TIP] 判断顺序很重要
> 实际代码中先比较 `min_trx_id` 和 `max_trx_id` 这两条边界,因为这是 O(1) 的比较操作。只有在落在两个边界之间时,才去扫描 m_ids 列表做成员检查。这种设计在高并发场景下能显著减少 CPU 开销。
> 实际代码中先检查是否是自己改的(`trx_id == creator_trx_id`),然后比较 `min_trx_id` 和 `max_trx_id` 这两条边界,因为这些都是 O(1) 的比较操作。只有 trx_id 落在两个边界之间时,才去扫描 m_ids 列表做成员检查。这种分层判断设计在高并发场景下能显著减少 CPU 开销。
## RC 与 RR 的关键区别
> [!TIP] 一个生活化的比喻
> 想象你在一家餐厅看菜单:
> - **RC(读已提交)** = 每次抬头看菜单,都是最新的版本。厨师中途改了价格,你下次看就能看到新价格。
> - **RR(可重复读)** = 你坐下来那一刻菜单就「定格」了。不管厨师之后怎么改菜单,你这顿饭看到的永远是第一眼的版本。
核心差异就一句话:**ReadView 创建时机不同**。
```mermaid
flowchart LR
subgraph RC["RC: 每次 SELECT 新建 ReadView"]
@@ -260,23 +276,30 @@ sequenceDiagram
```sql
-- 会话 A(RR 模式,MySQL 默认)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
START TRANSACTION; -- 事务 T_A, trx_id = 500
SELECT * FROM accounts WHERE id = 1;
-- 读到 balance = 500
-- 第一次 SELECT → 创建 ReadView
-- m_ids = {500} (只有自己在跑)
-- min_trx_id = 500
-- max_trx_id = 501
-- 读到 balance = 500(行的 trx_id=400 < 500 → 早提交规则 → ✅ 可见)
-- 会话 B
START TRANSACTION;
START TRANSACTION; -- 事务 T_B, trx_id = 600
UPDATE accounts SET balance = 1000 WHERE id = 1;
COMMIT;
COMMIT; -- 行的 trx_id 被更新为 600
-- 会话 A(同一个事务内再次查)
SELECT * FROM accounts WHERE id = 1;
-- 仍然读到 500!← ReadView 不让看 T_B 的改动
-- RR 模式 → 复用第一次的 ReadView(m_ids={500}, max_trx_id=501)
-- 行的 trx_id=600 >= 501 → 晚启动规则 → ❌ 不可见
-- 沿 ROLL_PTR 找到旧版本 trx_id=400 < 500 → ✅ 可见
-- 所以仍然读到 balance = 500!
-- 但如果用当前读呢?
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 读到 1000 ← 当前读不走 MVCC,直接拿最新版本
-- 当前读不走 MVCC,直接读磁盘上的最新行 → 读到 balance = 1000
```
## 关联笔记