
MySQL 采用“先写日志再写磁盘数据文件”的机制在数据库领域被称为WALWrite-Ahead Logging预写式日志。设计这一机制的核心原因只有一句话用极快的磁盘“顺序写”替代极其缓慢的磁盘“随机写”先顺序写日志解决当下的写入延迟再异步刷回 B 树保障后续的读取效率正是数据库在“读”与“写”之间达成的精妙平衡。一、 为什么不能直接把数据写回磁盘如果每次执行UPDATE、INSERT或DELETE都直接去修改磁盘上的数据文件.ibd文件数据库会立刻面临两个严重的性能灾难大量随机 I/O寻道极慢MySQLInnoDB 引擎的数据在磁盘上是按 B 树结构组织的。一个事务修改的几行数据物理位置往往分散在磁盘的不同角落。如果每次更新都直接落盘磁盘就需要不断地在不同位置之间跳转定位随机 I/O。无论是机械硬盘的磁头寻道还是 SSD 的随机写入随机 I/O 的速度都远低于顺序 I/O。严重的写放大改几个字节也要写 16KBInnoDB 与磁盘交互的最小单位是页Page默认大小为 16KB。哪怕你只是把某一行里的一个字段从1改成了2仅改动几个字节如果直接落盘也必须把这整整 16KB 的数据页完整写回磁盘性价比极低。二、 磁盘“顺序写”与“随机写”的速度到底差多少在真实生产环境中“顺序写日志”和“随机写数据页”的耗时差距在传统机械硬盘HDD上相差数百倍在现代固态硬盘SSD/NVMe上叠加 MySQL 自身的写入机制后实际耗时依然相差 10 倍到 100 倍以上。1. 硬件层面的单次耗时对比不同存储介质在进行随机写和顺序写时底层的物理耗时差距显著存储介质随机写单次延迟顺序写等效单次延迟持续写入吞吐量随机 vs 顺序速度差距倍数机械硬盘 (HDD, 7200转)约5∼10 ms5 \sim 10\text{ ms}5∼10ms(受限于磁头物理寻道盘片旋转)约0.01∼0.05 ms0.01 \sim 0.05\text{ ms}0.01∼0.05ms(磁头不动顺着磁道连续写)约1∼2 MB/s1 \sim 2\text{ MB/s}1∼2MB/svs.150∼200 MB/s150 \sim 200\text{ MB/s}150∼200MB/s100∼500100 \sim 500100∼500倍普通固态 (SATA SSD)约0.1∼0.5 ms0.1 \sim 0.5\text{ ms}0.1∼0.5ms(即100∼500 μs100 \sim 500\ \mu\text{s}100∼500μs)约0.01∼0.03 ms0.01 \sim 0.03\text{ ms}0.01∼0.03ms(即10∼30 μs10 \sim 30\ \mu\text{s}10∼30μs)约50∼100 MB/s50 \sim 100\text{ MB/s}50∼100MB/svs.500 MB/s500\text{ MB/s}500MB/s5∼155 \sim 155∼15倍高性能固态 (NVMe SSD)约20∼100 μs20 \sim 100\ \mu\text{s}20∼100μs(受限于闪存擦写块与垃圾回收)约2∼10 μs2 \sim 10\ \mu\text{s}2∼10μs(直接写入连续闪存页)约300∼600 MB/s300 \sim 600\text{ MB/s}300∼600MB/svs.3000∼7000 MB/s3000 \sim 7000\text{ MB/s}3000∼7000MB/s5∼105 \sim 105∼10倍以上为什么没有磁头的 SSD 随机写依然慢SSD 的闪存颗粒不能直接覆盖写必须**“先擦除、再写入”且擦除块Block通常几 MB远大于写入页Page通常 4KB~16KB。随机写会导致 SSD 内部产生大量碎片迫使主控芯片频繁执行垃圾回收GC搬运有效数据并擦除整块**引发严重的写放大和延迟抖动。2. MySQL 架构层面的三重耗时放大除了硬件本身的差距在 MySQL 中直接写数据页还会触发三重开销放大写入体积放大差 64 倍写 Redo Log 仅记录改动内容按扇区对齐最小仅需写512 字节而直接刷数据页是16KB加上防止半页损坏的 Doublewrite Buffer双写缓冲区实际落盘高达32KB。多索引连带惩罚1 次 vs. N 次一张表若有 1 个主键索引 2 个二级索引更新一行数据直接落盘需要随机写3 个不同的 B 树数据页3 次随机 I/O而写 Redo Log 只需把这 3 处改动打包成1 次顺序 I/O。高并发组提交Group Commit100 个并发事务同时提交时直接落盘需要执行上百次随机 I/O而 Redo Log 可以将这 100 个事务的日志在内存中合并通过1 次顺序写一次性刷入磁盘。3. 直观算账执行 10,000 次更新耗时对比假设单线程下连续执行10,00010,00010,000条带 2 个二级索引的UPDATE语句每次更新涉及 3 个数据页机械硬盘 (HDD)直接随机写需30,000×8 ms≈240 秒30,000 \times 8\text{ ms} \approx \mathbf{240\text{ 秒}}30,000×8ms≈240秒4 分钟而顺序写 Redo Log 仅需10,000×0.05 ms≈0.5 秒10,000 \times 0.05\text{ ms} \approx \mathbf{0.5\text{ 秒}}10,000×0.05ms≈0.5秒相差 480 倍。企业级 SSD直接随机写需30,000×0.2 ms≈6 秒30,000 \times 0.2\text{ ms} \approx \mathbf{6\text{ 秒}}30,000×0.2ms≈6秒而顺序写 Redo Log 仅需10,000×0.02 ms≈0.2 秒10,000 \times 0.02\text{ ms} \approx \mathbf{0.2\text{ 秒}}10,000×0.02ms≈0.2秒相差 30 倍。三、 “先写日志再落盘”是如何工作的为了绕开上述瓶颈InnoDB 引入了内存缓冲池Buffer Pool和重做日志Redo Log把一次数据更新拆成了三个阶段先改内存极快当需要更新一条数据时InnoDB 先在内存的Buffer Pool中找到对应的数据页并直接修改。此时内存里的数据是最新的而磁盘上的数据还是旧的这个内存页被称为脏页Dirty Page。再写日志顺序写磁盘快接着InnoDB 把“在哪个数据页上做了什么修改”生成一条紧凑的Redo Log记录追加写入到磁盘上的 Redo Log 文件末尾。因为永远是在文件末尾追加顺序 I/O不需要寻道且只记录改动内容通常只有几十到几百字节写入速度极快。只要 Redo Log 落盘成功事务就可以立刻向客户端返回“提交成功”。后台异步落盘批量合并省力至于内存里的“脏页”InnoDB 不会立刻刷入磁盘而是交由后台线程在系统空闲、脏页比例达到阈值、或者 Redo Log 快写满时分批、合并地刷回磁盘数据文件。合并写入的优势如果某个热点数据页在一秒内被修改了 1000 次采用 WAL 机制时只需要改 1000 次内存 顺序追加 1000 条极小的日志最后后台只需要把这 16KB 的数据页刷盘 1 次而如果直接落盘则需要执行 1000 次 16KB 的随机磁盘写入。四、 两种方式的直观对比对比维度直接修改磁盘数据文件先写 Redo Log 再异步落盘WAL磁盘 I/O 类型随机 I/O位置分散寻道开销极大顺序 I/O日志末尾追加写速度极快单次写入数据量整个数据页固定16KB含双写则为32KB仅记录修改内容通常几十至几百字节数据刷盘频率每次事务提交都必须同步刷盘后台异步刷盘多次修改可合并为一次刷盘用户体验响应耗时极慢高并发下磁盘瞬间打满极快写完内存和顺序日志即返回成功五、 脏页还没落盘服务器突然断电怎么办你可能会担心如果事务提交后只写了Redo Log内存里的脏页还没来得及刷入磁盘数据文件此时服务器突然断电重启内存里的数据全部消失数据会丢失吗不会丢失。这正是Redo Log存在的第二个核心价值——Crash-Safe崩溃恢复能力当 MySQL 重启时InnoDB 会自动扫描磁盘上的Redo Log文件。它会找出那些“已经写入日志、但还没来得及刷入数据文件”的记录把这些修改在内存中重新执行一遍重放Replay然后再刷回数据文件。此外在 MySQL 架构中InnoDB 引擎层的Redo Log还会与 Server 层的归档日志Binlog配合通过**两阶段提交Two-Phase Commit**机制确保主从复制与崩溃恢复的数据严丝合缝。六、 延伸思考既然顺序写日志这么快为什么不全部只写日志来保存数据看到这里一个很自然的问题是既然顺序追加写日志Redo Log这么快为什么还要费劲把数据刷回.ibd数据文件B 树干脆废除数据文件把所有数据都用顺序写日志的方式保存不行吗答案是绝对不行。只写日志虽然能让“写入”达到极限速度但会彻底摧毁数据库的查询性能、存储空间和恢复效率查询读性能会变成灾难O(N)O(N)O(N)vs.O(logN)O(\log N)O(logN)日志是按“时间顺序”记录的流水账里面存的是“第 1 秒把id5的余额改为 100”、“第 800 秒把id5的余额改为 200”。如果你要执行SELECT balance FROM account WHERE id 5在没有内存缓存的情况下数据库必须从头到尾全盘扫描整个日志文件时间复杂度O(N)O(N)O(N)才能拼凑出id5的最新状态。数据文件.ibd是按“索引结构B 树”组织的尽管刷盘慢但一旦落盘整理成 B 树哪怕表里有 2000 万行数据查询任意一行也只需要3~4 次磁盘定位时间复杂度O(logN)O(\log N)O(logN)并且能极快地支持范围查询BETWEEN、和排序ORDER BY。磁盘存储空间会无限膨胀如果一行数据被更新了 10 万次在.ibd数据文件的 B 树里它永远只占1 行的空间原地覆盖更新。但如果只用追加日志来存磁盘上就会永久保留这10 万条历史变更记录几 GB 的真实数据很快就会膨胀成几 TB 的日志文件。这也是为什么 InnoDB 的Redo Log被设计成固定大小、循环覆盖写的环形文件——一旦历史日志对应的脏页已经刷入.ibd数据文件这部分旧日志就会被擦除重用。崩溃重启的时间会完全失控如果没有后台把脏页刷入数据文件即推进 Checkpoint 检查点一旦数据库重启想要在内存中恢复出最新数据MySQL 就必须把从建库第一天起积累的几百 GB 甚至上 TB 的日志全部重放Replay一遍重启一次可能需要几个小时甚至几天。有了定期落盘机制后MySQL 重启时只需要重放最后一次刷盘点Checkpoint之后的那一小段日志通常几秒钟就能完成恢复。总结来说日志Redo Log是为“极速写入与恢复”而生的而数据文件B 树是为“极速查询与紧凑存储”而生的。先顺序写日志解决当下的写入延迟再异步刷回 B 树保障后续的读取效率正是数据库在“读”与“写”之间达成的精妙平衡。