This commit is contained in:
2026-05-24 11:42:38 +08:00
commit 30d312ac35
521 changed files with 146481 additions and 0 deletions
@@ -0,0 +1,296 @@
---
tags: [MySQL, ACID, Redo Log, Undo Log, durability, binlog, MVCC]
create time: 2026-05-16 00:00
---
# ACID 与原子性实现
## 概述
ACID 是事务的四大特性——Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)。本节聚焦原子性和持久性的底层实现机制,回答「MySQL 如何保证你提交的修改不会丢失、回滚的操作真的撤销了」。
## ACID 全景
```mermaid
flowchart LR
A["Atomicity<br/>原子性"] -->|"Undo Log 回滚"| T1["要么全部完成<br/>要么全部不做"]
C["Consistency<br/>一致性"] -->|"约束 + 事务组合"| T2["数据库从一个合法状态到另一个合法状态"]
I["Isolation<br/>隔离性"] -->|"MVCC + Lock"| T3["并发事务互不干扰"]
D["Durability<br/>持久性"] -->|"Redo Log 刷盘"| T4["一旦提交永不丢失"]
style A fill:#C44569,color:#fff
style D fill:#EE5A24,color:#fff
```
> [!NOTE] Consistency 是目标,其他三者是手段
> Atomicity、Isolation、Durability 是 InnoDB 提供的技术保障,而 Consistency(业务层面的一致性)需要应用层通过合理的事务设计来实现。InnoDB 保证你提交的 SQL 不会丢、不会乱,但「转账后余额对不对」是你的责任。
### Consistency —— 一致性如何落地
Consistency 不是靠某个单独的日志机制实现的,而是多种约束机制叠加的结果:
| 层级 | 约束类型 | 示例 | 强制时机 |
|------|----------|------|----------|
| **表级** | PRIMARY KEY | `id INT AUTO_INCREMENT PRIMARY KEY` | INSERT / UPDATE |
| | UNIQUE | `email VARCHAR(255) UNIQUE` | INSERT / UPDATE |
| | NOT NULL | `balance DECIMAL(10,2) NOT NULL` | INSERT / UPDATE |
| | FOREIGN KEY | `user_id INT REFERENCES users(id)` | INSERT / UPDATE / DELETE |
| | CHECK | `balance >= 0` (MySQL 8.0.16+) | INSERT / UPDATE |
| **行级** | Row locks | `SELECT ... FOR UPDATE` | 查询时持锁 |
| **事务级** | SERIALIZABLE 隔离级别 | 整个事务串行化执行 | 事务执行期间 |
```sql
-- 约束是 Consistency 的第一道防线
CREATE TABLE accounts (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
balance DECIMAL(10,2) NOT NULL DEFAULT 0 CHECK (balance >= 0),
UNIQUE KEY uk_user (user_id),
CONSTRAINT fk_accounts_user FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 即使有约束,业务一致性仍需事务保障
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; -- A 转出
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2; -- B 转入
COMMIT;
```
> [!QUESTION] 如果第二条 UPDATE 失败了怎么办?
> 单条 UPDATE 失败只影响本条语句,但如果放在事务中,整个事务可以 ROLLBACK —— 这正是原子性为一致性保驾护航的场景。**三者的关系:原子性是手段,一致性是结果。**
---
## Redo Log —— 保证持久性
Redo Log(重做日志)是 InnoDB 特有的**物理日志**,记录的是「在哪个数据页的什么位置改了什么字节」。
### Redo Log 的工作流程
```mermaid
sequenceDiagram
participant TX as 事务 A
participant BP as Buffer Pool
participant RL as Redo Log Buffer
participant Disk as Redo Log File(磁盘)
TX->>BP: UPDATE accounts SET balance=100 WHERE id=1
Note over BP: 找到数据页 → 修改内存中的值<br/>→ 标记为脏页(dirty page)
BP->>RL: 写入 Redo Log entry<br/>(LSN+1)
alt "innodb_flush_log_at_trx_commit = 1"
RL->>Disk: sync() 强制刷盘 ✅
else innodb_flush_log_at_trx_commit = 2
RL->>OS: 写入 OS Page Cache
Note over Disk: 每秒由后台线程刷盘
end
TX->>TX: COMMIT 返回成功
```
### LSN (Log Sequence Number)
每条 Redo Log 都有一个单调递增的 LSN,用于追踪和排序:
```go
// InnoDB 内部数据结构伪代码
type redo_log_entry struct {
lsn uint64 // 全局唯一的日志序号
page_id uint32 // 被修改的数据页编号
offset uint16 // 页内的偏移量
data []byte // 实际修改的字节(如: old_balance=500 → new_balance=100)
}
```
LSN 是 InnoDB 中最核心的序列号之一,它贯穿了 **Redo Log、Undo Log、数据页、Check Point** 等所有涉及「顺序」的场景。比较两个 LSN 就能判断哪个操作先发生——崩溃恢复全靠它排定先后。
> **理解关键**:Redo Log 记录的不是「把 balance 改成 100」这种 SQL 语义,而是「第 3 号数据页第 2048 字节,从 `0x1F4` 改成了 `0x64`」。这就是为什么叫物理日志 —— 恢复时直接按字节写回即可,不需要解析 SQL,也不需要重新做权限检查、约束校验。
### Redo Log 的两个关键属性
| 属性 | 说明 | 影响 |
|------|------|------|
| **循环写入** | 大小固定,写满后从头覆盖 | 不会无限增长占满磁盘 |
| **物理日志** | 记录页级别修改而非 SQL | 恢复速度快,不需要重执行 SQL |
```ini
# 配置文件
innodb_log_file_size = 512M # 每个 log file 大小,两个共 1GB
innodb_log_files_in_group = 2 # log group 中的文件数
innodb_max_undo_log_size = 1G # undo log 最大体积
```
> [!TIP] innodb_log_file_size 怎么设?
> - 默认各 48MB(太小!频繁 checkpoint)
> - 推荐 256M~2G(写入密集型可更大)
> - 越大意味着:checkpoint 间隔越长、崩溃恢复越慢、缓冲池可容纳更多脏页
> - **调大需要在停机窗口操作**(需要先删旧 log file,MySQL 会报错提示)
### 两阶段提交(Two-Phase Commit, 2PC)
这是保证 Binlog 和 Redo Log 一致性的核心协议:
```mermaid
sequenceDiagram
participant App as 应用
participant S as Server Layer
participant IB as InnoDB Engine
participant BGL as Binlog
participant RDL as Redo Log
App->>S: BEGIN → UPDATE → COMMIT
S->>IB: Prepare 事务
IB->>RDL: 写入 Redo Log (prepare 状态)
Note over S: Phase 1: Redo Log Prepared
S->>BGL: 写入 Binlog
Note over S: Phase 2: 确认提交
S->>IB: Commit 通知
IB->>RDL: 标记 Redo Log 为 committed
IB-->>S: 返回 OK
S-->>App: 事务提交成功
```
> [!QUESTION] 为什么要两阶段?
> 如果只写 Binlog 不写 Redo Log(或反过来),崩溃后会出现不一致:
>
> - **只有 Binlog 没有 Redo Log**:主从复制时,从库执行了 Binlog 但该事务从未在本库提交 → 数据错乱
> - **只有 Redo Log 没有 Binlog**:本库恢复了但主从不一致 → 从库缺少这条数据
>
> **中间态处理**:如果 Phase 1 写完准备日志后崩溃,恢复时会检查 Binlog 是否存在:
> - Binlog 存在 → Redo Log 标记为 committed → 恢复后提交
> - Binlog 不存在 → Redo Log 标记为 aborted → 恢复后回滚
## Undo Log —— 保证原子性
Undo Log(回滚日志)是 InnoDB 的**逻辑日志**,记录的是「数据修改前的样子」。
### Undo Log 的核心用途
| 用途 | 说明 |
|------|------|
| **事务回滚** | ROLLBACK 时读取 undo log 反向操作 |
| **MVCC 多版本读** | 构建 Read View 时的历史版本链 |
| **事务一致性快照** | 保证同一事务内多次读看到相同数据 |
```
原始数据:balance = 500
事务 A: UPDATE accounts SET balance = 100 WHERE id = 1
Undo Log 记录:
- undo_no: 1024
- prev_lsn: 12345678
- type: ROW_UPDATE
- table_id: 42
- row_id: 1
- before_image: {balance: 500} ← 修改前的值
rollback 时:用 before_image 恢复 balance = 500
```
Undo Log 本质上维护了一个**修改前镜像链(before-image chain)**。每次对某行做 UPDATE,InnoDB 会把修改前的整行数据(不只是变化的列)写入 undo log,然后在新行版本上应用修改。ROLLBACK 时,InnoDB 从 undo log 中沿着这个链逐个还原即可。
> **为什么是逻辑日志?** 因为 undo log 记录的是「对哪行的哪个字段做了什么操作」,而不是物理字节偏移。这使得 undo log 可以被重放到其他存储引擎,也是 MVCC 能够构建历史版本的基础。
### Rollback Segment
Undo Log 存储在专门的 Rollback Segment 中:
```ini
innodb_rollback_segments = 128 # 回滚段数量
innodb_max_purge_lag_delay = 0 # purge 延迟阈值(微秒)
```
> [!NOTE] Undo Log 的生命周期
> 1. 事务执行过程中持续追加 undo log
> 2. 事务 commit 后,undo log 不会立即删除
> 3. 直到**没有其他事务需要它做 MVCC 可见性判断**时,Purge Thread 才清理它
> 4. undo log 空间会被循环回收
## 崩溃恢复 (Crash Recovery) —— ACID 的最终保障
InnoDB 拥有强大的崩溃恢复机制:**每次重启,都会让数据库回到一致状态**。
### ARIES 恢复算法简述
```mermaid
flowchart TD
A[MySQL Start] --> B[Redo Analysis Stage]
B --> C[Scan Redo Log by LSN]
C --> D[TxId Page Dirty Flag Table]
D --> E{Committed?}
E -->|Yes: committed| F[Redo Forward Apply]
E -->|No: not committed| G[Undo Rollback]
F --> H[Data Pages Restored to Consistent State]
G --> H
H --> I[Recovery Complete<br/>DB Online]
style A fill:#4A90D9,color:#fff
style I fill:#50E3C2,color:#000
```
### 恢复流程图
```mermaid
sequenceDiagram
participant DB as Crash MySQL
participant RDL as Redo Log
participant UDL as Undo Log
participant DP as Data Pages(Disk)
Note over DB: Post-crash state<br/>dirty pages unflushed, tx status unclear
rect rgba(76, 175, 80, 0.15)
Note over DB,RDL: Phase 1 Analysis: rebuild tx state
DB->>DB: Build TxId Page Dirty Flag Table
end
rect rgba(255, 87, 34, 0.15)
Note over DB,RDL: Phase 2 Redo: forward-apply committed tx
loop by LSN order
DB->>DP: Reapply committed modifications
end
end
rect rgba(244, 67, 54, 0.15)
Note over DB,UDL: Phase 3 Undo: rollback uncommitted tx
loop reverse scan undo log
DB->>DP: Undo uncommitted changes
end
end
DB-->>DB: DB restored to consistent state
```
### 典型场景分析
| 崩溃时机 | Redo Log 状态 | Undo Log 状态 | 恢复行为 |
|----------|--------------|--------------|----------|
| **UPDATE 后、COMMIT 前** | 有 redo entry(未标记 committed) | 有 undo record | 回滚未提交事务 |
| **Prepare 后、Binlog 写入前** | prepare 状态 | 有 undo record | 检查 Binlog → 不存在则回滚 |
| **Prepare 后、Commit 通知前** | prepare 状态 | 有 undo record | 检查 Binlog → 存在则提交 |
| **Binlog 写完、Redo Commit 前** | Binlog 已写,redo uncommitted | 有 undo record | 通过 2PC 判断:提交 |
| ** COMMIT 返回后** | 已 committed,可能尚未刷盘 | 不再需要 | 若 redo 未刷盘:crash-safe 窗口内丢失(依赖 binlog replication) |
> [!TIP] crash-safe 的边界
>
> `innodb_flush_log_at_trx_commit = 1` 时,COMMIT 后 Redo Log 已刷盘,即使进程崩溃也不会丢数据(前提是操作系统没挂)。
>
> 但如果**整台机器断电**,且 `innodb_flush_log_at_trx_commit = 2`,最近一秒钟的数据可能丢失。这就是为什么金融系统必须设为 1。
---
## 关联笔记
- [[hhs/MySQL/06-事务与并发控制/24-隔离级别与可见性]] — ACID 中 Isolation 的具体实现
- [[hhs/MySQL/06-事务与并发控制/25-MVCC 原理]] — Undo Log 如何支撑多版本并发控制
- [[hhs/GORM/08-事务管理]] — GORM 的事务 API 与 MySQL 引擎层的对应关系