MySQL日志全解

我们的故事从一个简单的 UPDATE 事务开始。

为了在 极致的性能(内存)可靠(磁盘) 之间找到完美的平衡,InnoDB 内部有着一套复杂且环环相扣的机制。

到达引擎层之前还有很多步骤,本篇不涉及

Buffer Pool

Buffer Pool 是 InnoDB 在 内存中开辟的一块区域,用来 缓存磁盘上的数据页和索引页。它是 InnoDB 性能的基石。

为什么要有 Buffer Pool?

因为计算机世界存在着一条巨大的速度鸿沟——内存读写是纳秒级,而磁盘读写是毫秒级(慢几十万倍)

所以 InnoDB 为了性能,制定了一条规矩:“所有的操作,必须在内存中进行

  • 读数据: 必须先把磁盘页加载到 Buffer Pool,再由 CPU 读取。
  • 写数据: 不直接写硬盘! 而是先修改 Buffer Pool 里的页。

这里就诞生了“脏页”这个概念

当我们执行 一条 UPDATE 语句后:

  1. InnoDB 只改了 Buffer Pool 里的页,还没同步到磁盘。
  2. 此时,内存里的数据和磁盘上的数据是不一致的。
  3. 这个时候的内存页,就叫做 “脏页”

刷盘(Flush):InnoDB 有后台线程(如 MasterThread 或 PageCleanerThread)会在系统负载较低时,或者在特定时机(如 Checkpoint),把这些“脏页”异步地写回磁盘文件。

这就有了一个 问题:如果中途断电了怎么办?内存是易失性的,里面的数据会瞬间消失。

一个 UPDATE 事务的执行

我们的 UPDATE 事务登场了。它要在 Buffer Pool 中修改数据。

第 0 步:加锁

一开始它就面临着一个严峻的挑战:并发冲突。如果两个事务同时试图修改 id=1 的数据,谁先谁后?

于是,InnoDB 祭出了 机制,事务在执行 UPDATE 前,会先去向锁管理器申请锁。如果申请成功,才能继续后续的操作。如果失败,当前事务就会进入等待状态,直到超时或对方释放锁。

本文不涉及关于并发安全问题以及锁机制的详细内容

第 1 步:记录“后悔药”(Undo Log)

事务的原子性 是指一个事务中的操作,要么全部成功,要么全部失败。

所以如果事务执行一半失败了,或者用户执行了 ROLLBACK,事务就需要回滚。InnoDB 需要某种机制来撤销所有已做的修改。

这就有了我们的第一个日志,在修改数据之前,InnoDB 会先记录“如何撤销”这次修改。这就是 Undo Log (撤销日志)

它是一种 逻辑日志,记录的是“如何撤销刚才的修改”:

  • INSERT 会记录一条对应的 DELETE
  • DELETE 会记录一条对应的 INSERT
  • UPDATE 会记录一条反向的 UPDATE(旧值)。

在修改数据之前,InnoDB 会先把数据的“旧值”记录到 Undo Log 中。

值得一提的是,Undo Log 本身也是数据,它存储在 Undo 页中,Undo 页也和数据页一样,会被缓存在 Buffer Pool 中,也会变“脏”。

第 2 步:保护数据(Redo Log)

正如前文所说,内存具有易失性,一旦中途断电,BufferPool 中的数据会瞬间蒸发。

Undo Log 只能回滚,不能把丢失的数据找回来。更别提 Undo Log 本身也是数据,它也面临着丢失的问题。

所以为了实现事务的 持久性,MySQL 需要保证,在告诉用户事务“提交成功”之前,必须 把这个修改后的 16KB 的数据页,完完整整地写回到磁盘的数据文件中

这简直就是性能杀手:

  • 首先是 随机 I/O:数据库的数据页是散落在磁盘的各个角落的。你修改 id=1 可能在磁盘头部,修改 id=10000 可能在磁盘尾部。每次提交事务都要磁头乱跳或者寻址,速度极慢。
  • 其次就是 写放大: 你可能只改了一个 16KB 页面里的几个字节,但你必须把整个 16KB 的页重新写入磁盘。

而且这没有解决“崩溃恢复”的问题,断电重启后,数据还是丢了。

所以为了解决这个问题,InnoDB 引入了 Redo Log (重做日志),它是一种 物理日志,它记录的是“对数据页做了什么修改”,比如:“在 Page5 的偏移量 100 处,写入了’ABC’”。

目前为止,本文一直说的都是 InnoDB 引入,因为 Undo 和 Redo log 确实就是 InnoDB 独有的。

MySQL 采用的是 “可插拔”存储引擎架构,Server 层是它的“大脑”,负责“指挥”,它 不涉及数据的物理存储细节,核心负责 SQL 的解析、优化、执行,以及一些公共功能(比如全局日志(binlog),函数等等)。这一层是 MySQL 原生实现的,无法替换。

而存储引擎层是数据的物理落地层,负责数据的实际存储和检索 以及 引擎专属的特性实现,像 InnoDB 引擎就实现了 事务、行锁、MVCC、Redo/Undo Log、 索引组织表等。Server 层定义了一套通用的 Handler API,任何存储引擎只要适配了这套接口,就能无缝接入。

但只引入这么个东西,问题还是没有解决。

因此 InnoDB 还为它设计了一个特殊的工作机制——Write-Ahead Logging (WAL)(日志先行)

核心原则就是:对数据的修改,必须先写日志,再写实际的数据文件

  1. 修改数据时先在 Buffer Pool 中修改,同时 把这个修改动作写入内存中的 Redo Log Buffer
  2. 在事务提交时,必须确保 Redo Log 已经从 Redo Log Buffer 刷入到磁盘上的 Redo Log File
  3. 至于 Buffer Pool 中的“脏页”什么时候刷回磁盘,可以晚一点,不着急。

只要日志写进去了,就认为数据安全了。哪怕内存崩了,重启后也能根据日志把数据“重做”一遍。

innodb_flush_log_at_trx_commit 控制事务提交时 Redo Log 的刷盘策略:

  • 1:默认值,每次事务提交时,都必须将 Redo Log 从 buffer 同步写入到磁盘文件,才算提交成功。
  • 0:事务提交时,不立即将 Redo Log 写入磁盘,而是等待主线程每秒一次的刷盘操作。如果 MySQL 在这 1 秒内宕机,会丢失最多 1 秒的数据。
  • 2:事务提交时,只将 Redo Log 写入操作系统的页缓存,而不是直接写入磁盘文件。由操作系统决定何时将缓存刷到磁盘。如果只是 MySQL 进程宕机而操作系统未宕机,数据不会丢失;但如果服务器整机断电,则会丢失数据。

为什么能行?

  • 它将对数据文件的 随机 I/O,转换成了对日志文件的 顺序 I/O。脏页刷盘是随机分布在磁盘各处的(随机 I/O,很慢)。但写 Redo Log 永远是追加写入(顺序 I/O,快)。
  • 只要 Redo Log 刷盘了,就算数据库崩溃,“脏页”没来得及刷盘,重启时 MySQL 也可以根据 Redo Log 把“脏页”上的修改“重做”一遍,恢复到崩溃前的状态。

其实在 InnoDB 的 物理存储层面,也存在原子性问题。

想象一下 B+ 树的分裂:当你插入一条数据,导致叶子节点满了,InnoDB 需要申请一个新页,把旧页的一半数据挪过去,修改父节点的指针,甚至可能引起父节点的分裂。

这是一系列复杂的物理操作。如果在这个过程中断电了:新页申请了但没挂到树上,或者父指针指错了地方,整棵 B+ 树的索引结构就 彻底损坏 了。

为了保证底层物理结构的完整性,InnoDB 引入了 Mini-Transaction (MTR)

  • 一组对 B+ 树的物理修改(涉及多个页),被视为一个不可分割的单元。InnoDB 会在内存中将这组操作对应的 Redo Log 准备好。
  • 在 MTR 结束时,这一批 Redo Log 会被 原子地 拷贝到全局的 Redo Log Buffer 中。你要么在 Redo Log 里看到完整的“页分裂”过程,要么完全看不到。绝不会出现“只分裂了一半”的日志。

但需要注意,这里的 Redo Log 日志以及它的 WAL 机制都 只是为了持久化而做的性能优化方案,在系统正常运行时,Buffer Pool 中的脏页刷回磁盘,完全不需要看 Redo LogRedo Log 只有在“非正常”时刻(崩溃恢复)才会被读取

那既然 Redo Log 是顺序读取,速度很快,为什么不干脆让后台线程去读 Redo Log,然后根据日志内容更新磁盘上的数据文件呢?这样岂不是连“脏页”都不用管了。

“正常运行” 场景下,它有三个巨大的问题:

第一个是 合并同类项效应

  • Redo Log 记录的是“动作”: 如果你对同一个数据页修改了 100 次,Redo Log 会记录 100 条日志,如果要根据 Redo Log 刷盘,你需要读取这 100 条日志,然后对磁盘文件执行 100 次修改操作。
  • Buffer Pool 里的脏页记录的是“结果”:无论你修改了 Page A 多少次,在内存里的这个脏页,永远只有 1 个。它是这 100 次修改后的最终状态。等到要刷盘的时候,只需要把这个页写 1 次 磁盘。

第二点,Redo Log 只是把 写日志 变成了顺序 I/O,但它并没有把 应用到数据文件 这个动作变成顺序 I/O。即使你顺序读取了 Redo Log,知道了要改哪,但你要改的 目标文件(.ibd 数据文件)依然是散落在磁盘各处的,当你去更新数据文件时,磁头还是得到处跳。

相比之下,InnoDB 的后台刷脏页机制会做很多优化,比如它会检测哪些脏页在物理磁盘上是相邻的,尽量把它们一起刷,或者按照 Space ID 排序刷,这比盲目照着 Redo Log 刷要高效得多。

第三点,也是这个方案最不合理的一点:

Redo Log物理日志,它记录的是“在 Page X 的 Offset Y 写入了 Z”。它 不包含 整个数据页的完整内容。如果你想通过 Redo Log 去更新磁盘上的数据,你手里只有一条日志:“在偏移量 100 处写入 ‘A’”。你不能直接往磁盘写一个只有 ‘A’ 的文件。你必须先把 原始数据页 从磁盘读出来。把 ‘A’ 拼接到这个页里。再把这个页写回磁盘。这就尴尬了,既然这个“修改后的完整页”已经在 Buffer Pool 里躺着了,你直接把它写下去不就好了吗?为什么非要“读日志 -> 读磁盘旧页 -> 拼装 -> 写盘”这么折腾一圈呢?

Redo Log 在磁盘上的存储形式比较特别,磁盘上的物理文件,通常命名为 ib_logfile0ib_logfile1 等。这些文件构成一个逻辑上的环形队列,从头到尾循环写入,当写到末尾时,会回到开头覆盖旧的日志。

  • InnoDB 使用 writepos(当前写入位置)和 checkpoint(擦除点)两个指针来管理这个环形缓冲区。
  • writepos 追上 checkpoint 时,表示 Redo Log 空间耗尽。
  • 此时 MySQL 必须 阻塞所有更新操作,强制将 Buffer Pool 中的脏页刷盘,以推进 checkpoint 释放日志空间(这被称为 “FuriosFlushing“,是性能抖动的常见原因)。

为了避免这种“性能抖动”,InnoDB 引入了 Adaptive Flushing (自适应刷盘) 算法。

  • 后台线程会持续监控两个核心指标:Redo Log 的生成速率脏页的产生速率
  • InnoDB 不需要等到 Redo Log 快满才动手。它会根据当前的生成速率,计算出一个合适的“刷盘速度”。如果 Log 涨得快,后台线程就默默加快刷脏页的频率;如果 Log 涨得慢,就降低频率节省 I/O。

这个机制保证了数据库的 I/O 负载是一条平滑的曲线,而不是锯齿状的波峰波谷,让业务感受不到后台的“忙碌”。

第 3 步:提交数据(Binlog 与 2PC)

客户端执行了 COMMIT,提交了事务。

这里需要补充一个背景,一般来说,我们不会只有一台 MySQL,而是有多台,形成一个集群,并实现 主从复制(Replication)——就是一台 MySQL 作为主库,其它作为从库,分担读数据的压力

MySQL 的 Server 层 通过 Binlog (归档/二进制 日志) 实现主从复制。它记录了 所有对数据库造成数据更改的 DDL(Data Definition Language,数据定义语言,建表/改结构)和 DML(Data Manipulation Language,数据操纵语言,增删改)

其实从这里就能看出,Binlog 必须设计在 Server 层,只有 Server 层才具备解析 SQL 的能力,它能区分什么是 DDL 什么是 DML。底层的存储引擎只负责执行具体的物理读写指令,无法感知上层的业务逻辑。

Binlog 是逻辑日志,而且有多种日志格式。

  1. STATEMENT :记录原始的 SQL 语句。优点是日志文件小,缺点是在某些情况下可能导致主从数据不一致(例如,SQL 中包含 UUID()NOW() 等不确定性函数)。
  2. ROW :记录每一行数据被修改的具体内容。这是目前 默认且推荐 的格式。优点是能精确地记录数据变更,保证主从一致性,但 缺点是日志文件可能会变得很大
  3. MIXED:是 STATEMENTROW 的混合模式。MySQL 会根据执行的 SQL 语句,自动选择更合适的格式。一般情况下使用 STATEMENT,但在可能导致数据不一致时,会自动切换到 ROW 格式。

Binlog 的写入也是先写内存,每个线程都有一个独立的 binlog cache

如果事务过大,超过了 binlog_cache_size 设置的大小,MySQL 会被迫将多余的数据写入 临时文件。这会导致磁盘 I/O 飙升。因此,对于经常有大事务(如大批量导入、大字段更新)的系统,需要适当调大此参数。

sync_binlog 参数控制 Binlog 的刷盘策略:

  • 0默认值,但在高可用环境下不推荐。事务提交后,Binlog 只写入操作系统的页缓存,由操作系统决定何时刷盘。性能最好,但服务器宕机时 Binlog 可能会丢失。
  • 1:每次事务提交,都将 Binlog 同步写入磁盘。最安全,但对性能有一定影响。
  • N(N > 1):每 N 次事务提交,才将 Binlog 同步写入磁盘。

所谓双 1 配置就是指的把 innodb_flush_log_at_trx_commitsync_binlog 两个都配置为 1,这样每次事务提交时,都会同步把 Redo LogBinlog 写入磁盘,一致性最高。

在主从架构中,主库将其 Binlog 传递给从库,从库接收并重放 Binlog 中的事件,从而实现与主库的数据同步(主从复制)。

利用 Binlog 还能进行精确的 数据恢复——通过使用全量备份(如 mysqldump)加上 Binlog,可以将数据库 恢复到任意一个精确的时间点先恢复全量备份,然后重放指定时间范围内的 Binlog 事件)。

以及一个让 Binlog 在 MySQL 里几乎无法被去掉的场景——审计(Auditing)

在现代大数据架构中,Binlog 的地位甚至已经超越了 MySQL 本身。其作用不仅是复制,更是 CDC (Change Data Capture)

大数据(Flink/Spark)、搜索引擎(Elasticsearch)、缓存(Redis)的数据更新,几乎全靠监听 MySQL 的 Binlog(通过 Canal 或 Debezium)。通过分析 Binlog 的内容,追踪数据库的所有数据变更历史,进行数据分析与问题排查。

Binlog 为什么不能用来做 Crash Recovery?

假设 MySQL 崩溃了。重启时,Binlog 里记录着:“给 ID = 2 的 c 字段加 1”。

此时磁盘上的数据页可能有两种状态:

  1. 已落盘:该操作对应的脏页已经刷入磁盘。
  2. 未落盘:该操作仅在内存中完成,磁盘上的数据页还是旧的。

Binlog 是追加写的,它只知道操作发生了,但 不知道这个操作对应的物理数据页是否已经真正写入了磁盘

这就是 Binlog 的问题所在,它没法告诉你数据页的状态

同时因为 Binlog 是 逻辑日志。所以它 无法保证幂等性(Idempotency)。如果 MySQL 重启后强制重放 Binlog,对于“已落盘”的数据页,再次重放会导致数据被 重复修改(例如值从 2 变成 3,再变成 4),从而导致数据错乱。

Redo Log 记录的是物理修改(其实也不是纯物理日志),所以主要还是靠数据页头部的 LSN 作为判断依据,以此来实现 Idempotency。

既然有主从架构,那就会有主从切换。在传统的 MySQL 主从复制中,从库同步数据依赖的是“文件名 + 物理偏移量”(例如:master-bin.00032 pos 45602),被称为 物理坐标。这简直是运维的噩梦,因为一旦主库宕机,新主库上位,从库必须手动计算并重新指向新的物理位置。这个过程极易出错,稍有偏差就会导致数据丢失或同步中断。

现代 MySQL 引入了 GTID (全局事务标识符) 来解决这个问题——每一个事务在提交时,都会被分配一个全局唯一的 ID(Server_UUID:Transaction_ID),这个 ID 会随着 Binlog 传遍整个集群,且在整个生命周期内保持不变。在主从切换时,从库只需要对新主库说:“我有 GTID 1 到 100 的数据,后面的全给我。”新主库就能自动计算出缺少的数据并发送过去。GTID 屏蔽了底层的物理文件位置,让高可用架构(如 MHA、Orchestrator)的实现变得简单可靠。

在 MySQL 的分层架构中,Server 层和引擎层各自维护了一份日志:

  • Binlog(归档日志):属于 Server 层(上层)。它是下游(从库、数据分析)的数据源头,决定了 “主从是否一致”
  • Redolog(重做日志):属于 InnoDB 引擎层(底层)。它是崩溃恢复的唯一依据,决定了 “本机是否不丢数据”

由于这两份日志由不同组件维护,如果不能保证它们 原子性 地写入成功,就会出现 数据一致性问题

这是一个发生在一个单机系统的 Server 层引擎层 之间的“分布式事务问题”。

假设我们执行一个 UPDATE 操作:

  • 场景 A(先写 Redolog,后写 Binlog):Redolog 写完后系统宕机,Binlog 没来得及写。

    • 结果:重启后,主库通过 Redolog 恢复了这行数据。但因为 Binlog 缺失,从库永远收不到这条更新。
    • 判定主从不一致(主库多,从库少)。
  • 场景 B(先写 Binlog,后写 Redolog):Binlog 写完后系统宕机,Redolog 没来得及写。

    • 结果:重启后,主库因为 Redolog 缺失,事务回滚(数据消失)。但从库收到了 Binlog,执行了更新。
    • 判定主从不一致(主库少,从库多)。

为了解决这个问题,MySQL 引入了内部 XA 事务机制,也就是 两阶段提交 (Two-Phase Commit, 2PC) 机制。

这是一种严密的协作协议,将事务提交拆解为两个阶段:

  1. Prepare 阶段

    1. InnoDB 将事务的 Redo Log 写入 Redo Log Buffer,并根据 innodb_flush_log_at_trx_commit 策略执行 fsync 刷入磁盘。
    2. InnoDB 将 Redo Log 中该事务的状态标记为 PREPARE,并记录全局事务 ID(XID)。
    3. 此时引擎层通知 Server 层:“数据已持久化,随时待命,可提交也可回滚。”
  2. Commit 阶段

    1. Server 层将 Binlog 写入 Binlog Cache,并根据 sync_binlog 策略执行 fsync 刷入磁盘。
      • Binlog 的成功落盘是事务成功的唯一“逻辑分水岭”。一旦 Binlog 成功落盘,此时事务在逻辑上已经成功。
    2. Server 层调用 InnoDB 接口通知提交。InnoDB 将 Redo Log 状态置为 COMMIT
      • 这一步通常仅需内存操作,无需强制刷盘。因为只要 Binlog 在,崩溃恢复时就能推导出该事务有效。

此时我们来看看 2PC 是怎么保证数据一致性的同时完成崩溃恢复的。

假设服务器在 COMMIT 刚发出或刚写完时突然断电,重启时,InnoDB 必须利用日志将数据库状态恢复到断电前的那一瞬间。

第一步:InnoDB 启动时,会去读取最近一次的 Checkpoint LSN。Checkpoint 保证了在此之前的脏页都已刷盘,因此无需恢复。

第二步:然后 InnoDB 从 Checkpoint LSN 开始,无差别地 应用所有 Redo Log。此时,Buffer Pool 被严格还原到宕机前一刻的状态(包含已提交和未提交的事务)。

第三步:此时内存中存在处于 PREPARE 状态的事务,InnoDB 需要决定是 提交 还是 回滚裁决的最高准则是:Binlog 的完整性。

那怎么判断一个事务的 Binlog 是否“完整”?MySQL 依靠的是特定的“结束标记”

ROW 格式下,一个完整的事务在 Binlog 物理文件中长这样:

  • HeaderGTID_EVENT / BEGIN
  • BodyTABLE_MAP_EVENT(操作了哪张表)
  • BodyWRITE_ROWS_EVENT / UPDATE_ROWS_EVENT(改了哪些行)
  • Footer(关键)XID_EVENT

MySQL 在扫描 Binlog 时,只有读到了最后的这个 XID_EVENT(它里面记录了事务的 XID),才认为这个事务是 “完整” 的

InnoDB 提取 Binlog 中所有拥有完整 XID_EVENT 的事务 ID,与 Redo Log 中的 PREPARE 事务进行核对:

  • 情况 A:Binlog 中存在该 XID
    • 场景推演:宕机发生在 2PC 的第二阶段(Binlog 已落盘,但 InnoDB 还没来得及改写 Commit 状态)。
    • 结论提交事务
    • 理由:Binlog 既然已经写入,说明从库可能已经同步了这条数据。为了保证主从一致,主库必须“认账”,将该事务在引擎层补置为 COMMIT 状态。
  • 情况 B:Binlog 中不存在该 XID
    • 场景推演:宕机发生在 2PC 的第一阶段与第二阶段之间(Redo Log 好了,但 Binlog 没写完或没刷盘)。
    • 结论回滚事务
    • 理由:Binlog 没写入,意味着这个事务在逻辑上失败了,且从库永远不会收到。为了保证一致性,主库必须利用 Undo Log 撤销该事务在内存中的修改。

从上面这个流程也能看出来,“双 1”配置(innodb_flush_log_at_trx_commit=1sync_binlog=1)是最安全的,但也意味着 COMMIT 时至少有两次 fsync,性能很差。

MySQL 5.6 引入了 Binary Log Group Commit (BLGC) (组提交)机制,将提交过程细分为三个队列阶段,允许多个并发事务“搭便车”:

也就是多个事务按顺序将自己的 Binlog 写入操作系统的文件缓存。其中一个事务成为组长,“代表”大家,调用一次 fsync,将整组事务刷入磁盘。将 N 次磁盘 I/O 合并为 1 次,极大地提升了并发吞吐量。所有“搭车”成功的事务,再按顺序通知 InnoDB 进行引擎层提交。

组提交机制不仅解决了 2PC 的性能问题,还带来了两个重要的参数权衡:

  • binlog_group_commit_sync_delay:为了多攒几个事务,故意等待多少微秒。
  • binlog_group_commit_sync_no_delay_count:为了多攒几个事务,故意等待凑齐多少个事务。

ok,我们的事务终于成功提交了!终于安全落盘了…吗?

第 4 步:终极保险(页断裂与双写)

还有一个更极端、更恐怖的场景:页断裂(逻辑意义上的“断裂”)。

Linux 文件系统页的大小默认是 4KB。而 MySQL InnoDB 存储引擎的页大小默认是 16KB

MySQL 程序运行在操作系统之上,依赖操作系统的文件系统与磁盘交互。

所以当 InnoDB 需要将内存中一个 16KB 的脏页刷到磁盘时,操作系统需要执行 4 次 4KB 的 I/O 写入操作。

关键问题在于,这 4 次 I/O 操作 并非原子操作,如果在这期间服务器突然断电或操作系统崩溃,磁盘上的这个 16KB 数据页就损坏了——它既不是修改前旧数据,也不是修改后的新数据,而是一种“半成品”状态,这就是 部分写 问题。

这种“页数据损坏”是致命的,并且 无法通过 Redo Log 来修复

因为 Redo Log 记录的是“对页做了什么修改”,它的前提是页本身结构是完整的,如果页都损坏了,重做修改毫无意义。

Redo Log 在磁盘上也是按块存储的,每块 512 字节。这与磁盘扇区大小一致,因此 Redo Log 的写入通常能保证原子性。

为了解决这个问题,InnoDB 引入了“双写”机制。简单来说,就是在把脏页真正写入分散的数据文件之前,先找个地方把它们 完整地备份 一遍。

脏页刷盘的流程变成了三步:

  1. 内存拷贝:脏页刷盘时,不直接写磁盘。而是先将脏页复制到内存中的 Doublewrite Buffer 区域(大小通常为 2MB)。
  2. 顺序写备份:当内存 DWB 攒满(例如 1MB)后,InnoDB 发起一次系统调用,将这批页 顺序写入 到磁盘上的系统表空间(ibdata)或独立的 .dblwr 文件中。
  3. 离散写落盘:只有在步骤 2 的备份落盘成功(fsync)后,InnoDB 才会根据脏页的表空间 ID 和页号,将其 随机写入 到真正的 .ibd 数据文件中。

在 8.0.20 之前,DWB 寄居在 ibdata1 共享表空间中,容易与 Undo Log、数据字典产生 I/O 争用。8.0.20 后,DWB 被分离为独立的 .dblwr 文件(由 innodb_doublewrite_dir 控制),显着降低了争用,提升了吞吐量。

无论在哪,DWB 的磁盘空间大小通常都是 2MB(即 128 个 16KB 的页),且这 2MB 空间在逻辑上(或物理上)分为两个 1MB 的插槽(Slot)。

在步骤 2 写入一个插槽的同时,内存 DWB 可以使用另一个插槽接收新的脏页,实现并发。

当 InnoDB 重启时,如果发现某个数据页已损坏(页断裂),它会先去 Doublewrite 磁盘区域找到该页的 完好副本,用副本覆盖坏页,然后再 开始执行 Redo Log 进行恢复。

DWB 虽然安全,但带来了一个代价:写放大。数据需要写两次磁盘(一次顺序,一次随机),I/O 压力翻倍。

现代的高端 SSD 硬件(如 Fusion-IO)或特定文件系统(如支持原子写的 XFS)可以保证 16KB 写入的原子性。如果底层硬件支持原子写(即保证 16KB 要么全写成功,要么全不写),那么“页断裂”问题就不复存在。在这种环境下,可以将 innodb_doublewrite 设置为 OFF。这将消除 50% 的冗余 I/O,大幅提升数据库的写性能。

Undo Log 的第二使命——MVCC

我们的事务终于安全落盘了,但 Undo Log 不会立即被删除,它还有第二个使命。在多用户并发访问数据库的现实世界中,它从“后悔药”摇身一变,成为了 MVCC (多版本并发控制) 的基石。

为了解决“读-写”冲突,避免读取数据时加锁阻塞,MySQL 利用 Undo Log 保留了数据的历史版本:

  • 写操作:修改数据时,旧版本并不直接覆盖,而是被移动到 Undo Log 中。
  • 读操作:根据事务开启的时间点,去读 Undo Log 里的“历史快照”。
  • 结果读不阻塞写,写不阻塞读

根据 MVCC 的需求,InnoDB 对 Undo Log 的生命周期管理如下:

  • Insert Undo Log:新插入的数据没有旧版本,事务提交后即可 立即删除
  • Update Undo Log:由于 MVCC 需要利用它构建历史快照,事务提交后 不能立即删除
  • Purge 线程:后台线程会持续监控,只有当确认全系统没有任何事务再需要这些旧版本时,才会真正清理这些日志并释放空间。

本篇不涉及 MVCC 的详细讲解


MySQL日志全解
https://gavinmo1.github.io/2026/03/06/MySQL日志全解/
作者
Gavin
发布于
2026年3月6日
许可协议