一、核心日志全景图

在设计角度上,我们可以将日志分为两类:

  1. 存储引擎层日志(InnoDB 特有):关注事务的原子性(Atomicity)和持久性(Durability),即 ACID 中的 A 和 D。
    • Redo Log(重做日志):保证事务持久性,崩溃恢复。
    • Undo Log(回滚日志):保证事务原子性,支持回滚和 MVCC。
  2. Server 层日志(MySQL 通用):关注归档、复制和审计。
    • Binlog(二进制日志):主从复制、数据恢复、审计。
    • Relay Log(中继日志):主从复制中的中间环节。
    • 其他:Error Log(错误)、Slow Query Log(慢查询)、General Log(全量查询)。

二、设计演变史:从“脆弱”到“坚不可摧”

这套系统不是第一天就设计好的,而是为了解决特定问题逐步演变的。我们可以将其演变过程分为四个阶段:

第一阶段:只有数据文件(无日志)—— “脆弱期”
  • 设计状态:数据直接写入磁盘的数据文件(.ibd)。
  • 痛点:
    1. 性能极差:每次修改都要随机写磁盘(Random I/O),速度受限于磁盘机械结构。
    2. 数据易丢失:如果写到一半断电,数据文件损坏,无法恢复。
  • 结论:必须引入机制来解耦“内存修改”和“磁盘持久化”。
第二阶段:引入 Binlog(归档日志)—— “复制与恢复的萌芽”
  • 背景:MySQL 早期需要主从复制和数据备份。
  • 设计:
    • 位置:Server 层(所有引擎通用)。
    • 内容:逻辑日志。记录的是 SQL 语句的原文(Statement)或行变更逻辑(Row)。例如:“UPDATE t SET a=2 WHERE id=1”。
    • 写入方式:追加写(Append Only),顺序 I/O,性能好。
  • 解决的问题:
    1. 主从复制:从库重放 Binlog 即可同步数据。
    2. 时间点恢复:通过全量备份 + Binlog 恢复到任意时刻。
  • 遗留缺陷(Crash-Safe 问题):
    • Binlog 是在事务提交后才写入的。
    • 如果发生“两阶段提交”前的崩溃,或者数据页还没刷盘就崩溃,仅靠 Binlog 无法保证已提交事务的数据一定在数据文件中(因为 Binlog 是逻辑的,重放成本高且无法精确还原物理页状态)。
    • 核心矛盾:为了性能,我们希望数据先缓存在内存,定期刷盘;但为了安全,我们希望写完立刻落盘。Binlog 解决了“记录”,没完美解决“高效崩溃恢复”。
第三阶段:引入 Redo Log(重做日志)—— “崩溃恢复的终极方案”
  • 背景:InnoDB 引擎诞生,急需解决 Crash-Safe 问题,并进一步提升写入性能。
  • 设计:
    • 位置:存储引擎层(InnoDB 特有)。
    • 内容:物理日志。记录的是“在某个数据页上做了什么修改”(例如:在 Page X 的 Offset Y 处写入了值 Z)。
    • 机制:WAL (Write-Ahead Logging) 技术。
      1. 修改数据时,先写内存(Buffer Pool)。
      2. 同时写 Redo Log(顺序写,极快)。
      3. 事务提交时,只要 Redo Log 落盘,就算成功。
      4. 数据页可以在后台慢慢刷入磁盘(Checkpoint 机制)。
  • 演变意义:
    • 变随机写为顺序写:极大提升了事务提交速度。
    • 崩溃恢复:重启时,只需重放 Redo Log 即可将数据页恢复到崩溃前的状态,无需依赖昂贵的 Binlog 重放。
  • 新挑战:有了 Redo Log,事务能恢复了,但如果用户执行了 ROLLBACK 怎么办?Redo Log 只能“重做”,不能“撤销”。而且,并发读取时,如何看到未提交的历史版本?
第四阶段:引入 Undo Log(回滚日志)—— “原子性与多版本的拼图”
  • 背景:完善事务的原子性(Atomicity)和隔离性(Isolation)。
  • 设计:
    • 位置:存储引擎层(InnoDB)。
    • 内容:逻辑日志。记录的是相反的操作。
      • Insert -> 记录 Delete 逻辑。
      • Update -> 记录反向 Update 逻辑(旧值)。
      • Delete -> 记录 Insert 逻辑。
  • 演变意义:
    1. 事务回滚:如果事务失败,利用 Undo Log 反向操作,恢复数据原状。
    2. MVCC(多版本并发控制):配合 Read View,让事务能看到历史版本的数据,实现非阻塞读(快照读),大幅提升并发性能.

三、深度解析:各日志的核心设计与协作

1. Redo Log vs Binlog:为什么需要两份日志?

这是面试和架构设计中最经典的问题。既然 Redo Log 能崩溃恢复,为什么还要 Binlog?

表格

特性Redo Log (重做日志)Binlog (归档日志)
所属层级存储引擎层 (InnoDB)Server 层 (所有引擎)
日志类型物理日志 (记录页的修改)逻辑日志 (记录 SQL 或行变更)
写入方式循环写 (空间固定,写完覆盖)追加写 (文件写完切换新文件)
核心作用Crash-Safe (崩溃恢复)主从复制、数据归档恢复
写入时机事务进行中持续写入,提交时强制刷盘事务提交时一次性写入
删除策略自动覆盖,不保留历史永久保留,除非手动清理

设计哲学上的互补:

  • Redo Log 是为了“快”和“稳”:它让 InnoDB 可以随意地在内存修改数据,只要日志记下来就行,不用每次都刷数据页。它是短期的、循环的。
  • Binlog 是为了“广”和“久”:它作为整个 MySQL 实例的“黑匣子”,记录了所有变更历史,用于跨实例复制和长期归档。它是长期的、累积的。

两阶段提交(2PC):
为了保证 Redo Log 和 Binlog 的一致性(避免主从数据不一致),MySQL 引入了两阶段提交:

  1. Prepare 阶段:写 Redo Log,标记为 prepare 状态。
  2. Write 阶段:写 Binlog。
  3. Commit 阶段:提交 Redo Log,标记为 commit 状态。
    只有当 Binlog 写成功,Redo Log 才会最终提交。 这确保了:如果崩溃,重启时检查 Redo Log,若发现是 prepare 状态,则去查 Binlog,如果 Binlog 里有该事务,则重做;如果没有,则回滚。
2. Undo Log:隐藏的功臣
  • 设计细节:Undo Log 也是随事务产生的。事务提交后,Undo Log 不会立即删除,而是根据是否需要用于 MVCC 快照读取来决定何时 purge(清理)。
  • 价值:没有 Undo Log,就没有真正的在线热备和高并发读写分离。
3. Relay Log:主从复制的桥梁
  • 场景:在主从架构中,从库(Slave)不能直接读主库(Master)的 Binlog(网络开销、锁竞争)。
  • 设计:
    1. Master 将 Binlog 发送给 Slave 的 I/O 线程。
    2. Slave 将收到的 Binlog 写入本地的 Relay Log。
    3. Slave 的 SQL 线程读取 Relay Log 并重放执行。
  • 意义:解耦了“网络接收”和“SQL 执行”,提高了从库的鲁棒性和性能。

四、总结:从设计角度的启示

MySQL 日志系统的演变告诉我们几个核心的系统设计原则:

  1. 空间换时间,顺序换随机:
    • 通过引入 Log(Redo/Binlog),将随机的数据页写入变成了顺序的日志追加写,极大地利用了磁盘顺序 I/O 的高性能。
  2. 分层解耦:
    • Server 层负责通用逻辑(Binlog),引擎层负责具体实现(Redo/Undo)。这使得 MySQL 可以支持多种存储引擎,同时保持复制功能通用。
  3. 冗余与一致性权衡:
    • Redo Log 和 Binlog 看似冗余,实则职责分离。通过 2PC 机制,在保证数据强一致性的前提下,兼顾了恢复效率(Redo)和扩展能力(Binlog)。
  4. 以“恢复”为导向的设计:
    • 所有的日志设计,本质上都是在回答一个问题:“如果现在断电了,我该怎么以最小的代价把数据恢复到正确状态?”

涉及日志:Undo Log、Redo Log、Binlog
Relay Log:不涉及(主库没有 Relay Log)

🟢 写入顺序全景图

📝 详细步骤解析
  1. Undo Log 最先写入(伴随修改)

    • 时机:在事务执行过程中,只要发生了数据修改(INSERT/UPDATE/DELETE),InnoDB 会立即生成 Undo Log。
    • 原因:为了支持随时可能的 ROLLBACK 和 MVCC 快照读。如果这时候不写,用户一回滚就无法恢复旧数据了。
    • 位置:先写内存(Undo Log Buffer),通常随事务提交或后台线程刷入磁盘(ibdata1 或 undo 表空间)。
  2. Redo Log 写入(WAL 机制)

    • 时机:在修改内存中的数据页(Buffer Pool)时,同时生成 Redo Log 记录放入 Redo Log Buffer。
    • 注意:此时只是内存操作,还没强制刷盘。
  3. 两阶段提交(2PC)—— 关键的刷盘顺序
    当用户发出 COMMIT 指令时,严格的顺序如下:

    • Step A (Prepare):InnoDB 将 Redo Log Buffer 中的内容刷入磁盘,并将状态标记为 PREPARE。此时事务尚未真正提交。
    • Step B (Write):InnoDB 通知 Server 层,Server 层将 Binlog 写入文件系统并执行 fsync 刷入磁盘。
    • Step C (Commit):只有当 Binlog 落盘成功后,InnoDB 才会将 Redo Log 的状态修改为 COMMIT 并再次刷盘(或仅修改内存标记,取决于配置)。

核心结论(主库):
Undo Log (修改时) -> Redo Log (Prepare) -> Binlog -> Redo Log (Commit)


场景二:从库(Slave)同步事务

涉及日志:Relay Log、Redo Log、Undo Log、Binlog (从库通常不开启,若开启则最后写)
核心角色:Relay Log 是主角。

从库不直接接收主库的 Redo Log 或 Undo Log,它只接收主库的 Binlog。

🔵 写入顺序全景图

图表

📝 详细步骤解析
  1. Relay Log 写入(网络接收阶段)

    • 时机:从库的 I/O 线程 连接到主库,拉取主库的 Binlog 事件。
    • 动作:直接将收到的事件追加写入到本地的 Relay Log 文件中。
    • 特点:此时不执行任何 SQL,也不生成 Redo/Undo。这只是单纯的“搬运”工作,速度极快。
  2. Redo/Undo 写入(SQL 执行阶段)

    • 时机:从库的 SQL 线程 读取 Relay Log 中的事件。
    • 动作:在从库本地重放这些操作(就像有人在从库敲了一遍 SQL 一样)。
    • 内部顺序:一旦开始重放,从库内部的 InnoDB 引擎会完全重复主库的逻辑:
      1. 先写 Undo Log(为了原子性)。
      2. 再写 Redo Log (Prepare)。
      3. 由于这是重放过程,通常不需要再写 Binlog(除非从库开启了 log_slave_updates,用于级联复制)。
      4. 最后写 Redo Log (Commit)。

核心结论(从库):
Relay Log (最先,由 I/O 线程写入) -> Undo Log (执行时) -> Redo Log (Prepare) -> Redo Log (Commit)
(注:从库默认不写 Binlog,若开启则在 Redo Commit 前后)


四者关系总结表

表格

日志类型写入主体写入时机 (相对顺序)核心目的是否循环写
Undo LogInnoDB 引擎最早 (数据修改发生时)事务回滚、MVCC否 (用完可清理)
Redo LogInnoDB 引擎中间 (COMMIT 时的 Prepare & Commit)崩溃恢复 (Crash-Safe)是 (固定大小循环写)
BinlogMySQL Server中间偏后 (Redo Prepare 之后,Commit 之前)主从复制、归档否 (追加写,文件切换)
Relay Log从库 I/O 线程从库最先 (接收到主库 Binlog 时)缓冲主从网络差异否 (追加写,执行后清理)

关键设计点回顾

  1. 为什么 Undo Log 最早?

    • 因为事务随时可能失败。如果在修改数据前不预留“后悔药”(Undo Log),一旦报错就无法原子性回滚了。
  2. 为什么 Redo Log 分两阶段,夹着 Binlog?

    • 这是为了一致性。
    • 如果先写 Redo Commit 再写 Binlog:断电导致 Binlog 没写上,重启后 Redo 已提交,主库有数据但从库没有 -> 主从不一致。
    • 如果先写 Binlog 再写 Redo Prepare:断电导致 Redo 没写上,重启后 Binlog 有记录但 Redo 没印象,无法恢复 -> 数据丢失。
    • 夹在中间(2PC) 确保了:要么两者都有,要么两者都无(通过恢复逻辑回滚)。
  3. Relay Log 为什么独立存在?

    • 它将“网络 IO”(慢、不稳定)与“磁盘执行”(快、稳定)解耦。I/O 线程只管把主库的 Binlog 搬到 Relay Log 就完工,剩下的交给 SQL 线程慢慢从 Relay Log 读出来执行(触发内部的 Undo/Redo 流程)。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐