大家好,我是十三!欢迎来到十三Tech。

前几年压测一个写入密集的场景——为了模拟崩溃恢复,我用 kill -9 强杀了 mysqld。重启之后第一件事是对账:把应用侧记录的"成功写入"列表跟数据库实际数据对比。结果是——一条都没丢

这件事让我开始研究 InnoDB 怎么保证崩溃不丢数据。后来才搞懂,秘诀是一个叫 redo log 的东西,以及它背后的 WAL(Write-Ahead Logging)思想

很多人讲 redo log 都讲成"崩溃恢复日志"——这没错但太浅。redo log 真正的价值是改变了写数据的路径:先写日志、再写数据页。这个改变让 MySQL 在保持事务持久性的同时,吞吐量能跟上现代硬件。

这一篇就聊聊 redo log 的设计精髓、循环写结构、以及为什么它能让 kill -9 不丢数据。

redo log:MySQL 不丢数据的关键

一、WAL——为什么"先写日志"反而更快

先理解 redo log 在解决什么问题。

假设没有 redo log,InnoDB 的写入流程是这样的:每条 UPDATE 都直接改磁盘上的数据页。问题在于——数据页是 16KB,一次 UPDATE 可能只改几个字节,但写磁盘是整页写。如果每次 UPDATE 都 fsync 一次 16KB,性能会差到无法用。

更糟的是,fsync 是物理写——必须等磁盘真正落盘才能返回。100 IOPS 的磁盘意味着每秒最多 100 次事务,这在 OLTP 场景下根本不够用。

WAL 的解决方案是——把"对数据页的修改"先记到日志里(redo log),日志追加写、顺序 IO,性能极高;数据页的修改异步刷盘

传统方式:UPDATE → 改数据页 → fsync 数据页(慢)
WAL 方式:UPDATE → 写 redo log → fsync log(快)→ 后台异步刷数据页

WAL 的精髓:用顺序 IO 替代随机 IO。磁盘顺序写比随机写快 100 倍以上——这是 redo log 性能优势的根本。

但 WAL 引入一个新问题:数据页可能还没落盘,机器就崩了,怎么办? 答案就是崩溃恢复时重放 redo log——把所有"记下了但没落盘"的修改重新应用到数据页。这就是为什么 redo log 叫 redo(重做)。

WAL:先写日志后写数据页

二、循环写结构——为什么 redo log 不会无限增长

redo log 的文件大小是固定的——典型配置是 ib_logfile0ib_logfile1 两个文件、每个 48MB,加起来 96MB。这 96MB 是循环使用的——写满之后从头开始覆盖。

循环写的实现靠两个指针:

  • write position:当前写入位置(不断推进)
  • checkpoint:当前可以被覆盖的位置(数据页已经刷到磁盘的位置)

write position 追 checkpoint,checkpoint 追 write position——形成环形。如果 write position 快追上 checkpoint 了(说明 redo log 快写满),InnoDB 会强制触发 checkpoint,把对应的数据页刷盘。

这个设计有几个有趣的副作用:

副作用一:redo log 写满会让整库卡住。如果数据页刷盘跟不上 redo log 的写入速度,write position 追上 checkpoint,整个 InnoDB 必须"暂停写入"等 checkpoint 推进。生产中遇到这种情况,表现是 QPS 突然跌到零、innodb_log_waits 指标飙升。

副作用二:redo log 不能太小。太小意味着 checkpoint 频繁触发,写性能下降。MySQL 5.7 默认 48MB(每个文件),生产建议设到 1-4GB(取决于写入压力)。

副作用三:redo log 大小决定了"长事务能撑多久"。如果一个事务改了 1GB 数据但还没提交,redo log 里就有 1GB 没法回收——log 被占满,整库卡住。这就是为什么长事务在写入密集场景下特别危险。

redo log 的循环写结构

三、两阶段 checkpoint——崩溃恢复怎么知道重放到哪里

循环写有个核心问题——崩溃后怎么知道哪些 redo log 需要重放、哪些已经被 checkpoint 覆盖了

InnoDB 用 两阶段 checkpoint(sharp checkpoint + fuzzy checkpoint) 解决这个问题:

  • Sharp checkpoint:把所有 dirty pages 全刷盘,checkpoint 推到最新位置。只在关闭数据库时做。
  • Fuzzy checkpoint:异步地、增量地刷盘,checkpoint 逐步推进。运行期常态。

每次 checkpoint 推进,InnoDB 会在某个地方记录"LSN = X 时数据已经全部落盘"。崩溃重启后,从 LSN = X 的位置开始重放 redo log,直到最后一条。

LSN(Log Sequence Number)是 redo log 的"水位线"——单调递增的版本号。每个数据页头部记录自己的 LSN,checkpoint 也记录一个 LSN,对比两者能判断"这个页是新的还是旧的"。

崩溃恢复的完整流程:

  1. 读 checkpoint LSN,作为恢复起点
  2. 从这个位置开始扫 redo log,对每条记录判断"对应数据页的 LSN 是否比 redo log 的 LSN 小"——小就重做、大就跳过
  3. 重做完成后,数据库恢复到崩溃前的状态

这个过程在 MySQL 启动日志里能看到——InnoDB: Log scan progressed forward to ...Recovery: ... log sequence number ...

redo log 的崩溃恢复流程

回头看那个 kill -9 故事——为什么数据一条都没丢?因为我所有提交的事务,redo log 都已经 fsync 到磁盘(innodb_flush_log_at_trx_commit=1 是默认值)。崩溃后 InnoDB 重放这些 redo log,把还没落盘的数据页恢复回来。

redo log 真正教会我的,不是"崩溃恢复",而是用顺序 IO 替代随机 IO 的设计智慧——这个思想不只数据库在用,文件系统的 journal、消息队列的 commit log、分布式存储的 WAL 全是同一套。理解 redo log 等于理解了一大类系统的核心设计。

下一篇讲 binlog——它跟 redo log 长得很像但职责完全不同。


关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。

联系方式:569893882@qq.com GitHub:@TriTechAI