--- 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
原子性"] -->|"Undo Log 回滚"| T1["要么全部完成
要么全部不做"] C["Consistency
一致性"] -->|"约束 + 事务组合"| T2["数据库从一个合法状态到另一个合法状态"] I["Isolation
隔离性"] -->|"MVCC + Lock"| T3["并发事务互不干扰"] D["Durability
持久性"] -->|"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: 找到数据页 → 修改内存中的值
→ 标记为脏页(dirty page) BP->>RL: 写入 Redo Log entry
(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
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
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/24-隔离级别与可见性]] — ACID 中 Isolation 的具体实现 - [[hhs/MySQL/25-MVCC 原理]] — Undo Log 如何支撑多版本并发控制 - [[hhs/GORM/08-事务管理]] — GORM 的事务 API 与 MySQL 引擎层的对应关系