This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/06-事务与并发控制/23-ACID 与原子性实现.md
T
2026-05-21 19:31:28 +08:00

12 KiB
Raw Blame History

tags, create time
tags create time
MySQL
ACID
Redo Log
Undo Log
durability
binlog
MVCC
2026-05-16 00:00

ACID 与原子性实现

概述

ACID 是事务的四大特性——Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)。本节聚焦原子性和持久性的底层实现机制,回答「MySQL 如何保证你提交的修改不会丢失、回滚的操作真的撤销了」。

ACID 全景

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 隔离级别 整个事务串行化执行 事务执行期间
-- 约束是 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 的工作流程

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,用于追踪和排序:

// 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
# 配置文件
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 一致性的核心协议:

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 中:

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 恢复算法简述

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

恢复流程图

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。


关联笔记