面试冲刺复习手册:把三种存储放在同一个"日志先行"心智模型下对比,一次性理清 WAL / AOF / translog、快照与崩溃恢复。目标不是背参数,而是能讲清楚"每种日志为什么长这样"。
面试先说这句话,再往下展开 —— 能立住"我在讲原理而不是背八股"的人设。
磁盘随机写慢、顺序写快;数据量大的随机落盘无法每次同步完成。所以所有存储引擎的共同解法是:把"这次修改"先变成一小段顺序追加的日志落盘(快、可确认),数据本体异步慢慢刷;崩溃后用日志重放,找回没来得及刷盘的那部分。
这套骨架 = 内存缓冲 + 顺序日志(先写) + 异步落盘(快照/合并) + 崩溃重放。三者名字不同,本质同构:
数据页本体在磁盘,Buffer Pool 只是缓存。redo log 先于脏页刷盘,是真正的 WAL;另配 binlog(逻辑)、undo(回滚)。
数据本体在内存,落盘只是"备份"而非缓存管理。RDB 快照 + AOF 命令日志,纯追加、定期重写压缩。
段文件在磁盘但写入先走内存 buffer;translog 就是它的 WAL,refresh 让数据可搜、flush 才真正落盘。
MySQL:改磁盘数据页之前先写 redo(WAL),防止刷盘途中崩溃丢页
Redis:内存是本体,怕重启丢数据,所以记命令日志(AOF)+定期拍快照(RDB)
ES:segment 要攒批才落盘,空窗期靠 translog 兜底,refresh 只管"能不能搜到"
区分两个正交问题:① 数据要不要立刻可恢复(持久性)→ 日志负责;② 数据要不要立刻可见(对 MySQL 即事务可见性,对 ES 即可搜索性)→ 由各自的提交/refresh 机制负责。
redo/AOF/translog 都是尾部追加、顺序 IO,可预读可批量;而把 16KB 数据页(或若干 segment 数据)随机刷到磁盘各位置,机械盘磁头寻道、SSD 也有写放大。量级差可达 1~2 个数量级。
一次事务的日志往往几百字节到几 KB;被修改的数据页可能散布在多处、共几 MB。日志是"变更的最小充分描述"。
多个并发事务的 fsync 可以合并成一次(MySQL 组提交、ES translog 批量刷、Redis everysec)。确认一批事务只需一次落盘。
Redis 不落盘也能跑(纯缓存),此时"快照/日志"只是可选项;MySQL/ES 的数据本体在磁盘,WAL 是正确性的必需品 —— 这就是三者的本质差别来源。
本篇剩余部分:按系统拆解"日志记什么、何时落盘、崩溃怎么恢复",最后做横向对比和高频追问。
面试主战场。先分清 Server 层(binlog)与 InnoDB 引擎层(redo/undo),再讲两阶段提交。
InnoDB 把数据按 16KB 数据页存放在磁盘,Buffer Pool 缓存热页。若每次事务提交都把这些页同步刷回磁盘(随机 IO、页可能被改多次),性能不可接受。因此采用 WAL:
物理逻辑日志(physiological):记录"哪个表空间、哪个页号、页内偏移、改成了什么",围绕单个页内操作,不是整条 SQL、也不是整页镜像。循环写入一组文件(ib_logfile*,8.0.30+ 由 innodb_redo_log_capacity 管理)。它只管崩溃恢复(crash recovery),不管复制。
| 日志 | 层级 | 内容 | 用途 | 关键点 |
|---|---|---|---|---|
redo log | InnoDB 引擎层 | 物理逻辑:页号 + 页内修改 | 崩溃恢复:保证已提交事务的数据不丢(持久性),把 Buffer Pool 里没刷盘的页改回来 | 循环写、会覆盖;LSN 单调递增;checkpoint 推进后可覆盖;组提交合并 fsync |
binlog | Server 层 | 逻辑日志:完整 SQL 或行变更(statement / row / mixed,8.0 默认 row) |
主从复制 + 时间点恢复(PITR),也供 canal 等消费 | 追加写、不覆盖、可归档;与存储引擎无关;MyISAM 没有 redo 但有 binlog |
undo log | InnoDB 引擎层 | 修改前的旧版本数据(逻辑反操作) | 事务回滚 + MVCC 快照读:RR/RC 下读 undo 链构建历史版本 | 也落盘但属于"数据"而非 WAL 语义;purge 线程清理不再被任何事务看到的旧版本 |
① 层级不同:redo 在引擎层,只对 InnoDB;binlog 在 Server 层,对所有引擎、面向复制/恢复。② 内容不同:redo 是物理逻辑(页级),binlog 是逻辑(语句/行级)。③ 生命周期不同:redo 循环覆盖,binlog 追加保留。④ 时机不同:redo 在事务执行中就写(配合组提交),binlog 在事务提交时写。⑤ 语义不同:redo 保证"我告诉客户端提交了就不会丢",binlog 保证"别的主库/时间点能看到同样的数据"。
redo 与 binlog 是两个独立文件系统里的两份日志,写它们不是原子的。若先写 redo 后写 binlog,崩在中间:主库重放 redo 提交了事务,但从库没拿到 binlog → 主从不一致;反之则从库多执行。解法是经典的 prepare → commit 两阶段:
崩溃恢复时检查 redo 里的 prepare 事务:binlog 里若存在对应 XID 的事务 → 补做 commit(两库都执行了才一致);binlog 里没有 → 回滚。这样无论崩在哪个点,主库与从库(binlog 消费方)的提交集合都一致。注意这里 fsync 次数与组提交:binlog_group_commit 把多个事务的 binlog 写盘合并,innodb_flush_log_at_trx_commit 决定 redo 何时刷。
| 参数 | 取值 | 行为 | 崩溃丢多少 |
|---|---|---|---|
innodb_flush_log_at_trx_commit |
1(默认) | 每次事务提交都把 redo log buffer fsync 到磁盘 | 不丢(配合双写/页完整) |
2 | 每次提交只写到 OS page cache,由 OS 定期刷盘 | OS 不宕机不丢;主机断电最多丢约 1s | |
0 | 提交时不主动刷,由后台线程每秒刷一次 | MySQL 进程崩溃最多丢 1s 内已提交事务 |
同为 1(每次落盘)时,redo log 因为顺序写且可组提交,性能远好于直接刷随机数据页 —— 这就是 WAL 的意义。
checkpoint 是"这些 LSN 之前的脏页已刷盘"的标记,推进它 = 缩短恢复要重放的日志量 + 释放 redo 空间可覆盖。
redo 能重放的前提是目标页本身完整(它是"在这个页的修改上再改")。如果 16KB 页刷盘时断电只写了一半(torn page),redo 也救不了。所以 InnoDB 先把整页拷贝到连续的 doublewrite buffer(2MB 区)落盘,再写实际位置;恢复时若检测到半写页,从 doublewrite 副本还原。SSD 上可关(innodb_doublewrite=0)换吞吐,但默认开着。此点是区分"背过八股"和"真懂"的试金石。
刷脏页是随机 IO 且页大(16KB、涉及多个页);redo 是顺序追加、量小、可组提交合并 fsync。直接刷页 = 一次提交等多次随机写;写 redo = 一次提交等一次顺序写。前者吞吐低 1~2 个数量级。而且页可能被多个事务反复修改,日志只需记最终落盘前的增量,脏页刷盘频率远低于提交频率(checkpoint 周期化)。
循环文件。checkpoint 持续推进:脏页刷回磁盘后,对应 LSN 之前的 redo 空间即可复用。若刷脏页太慢、redo 写满,InnoDB 会强制推进 checkpoint(先刷脏页再允许覆盖),表现为写入被阻塞 —— 这就是 redo log busy / checkpoint age 类告警的来源。设计上让刷盘能力匹配写入量。
8.0 默认 row:记录每行变更前后值,主从结果确定、支持 DDL 安全;statement 只记 SQL,量小但依赖上下文(如 NOW()、LIMIT 更新)可能主从不一致。row 的代价是批量操作日志量大(可配 binlog_row_image)。canal/数据同步类中间件基本都要求 row。
先立观点:Redis 的持久化是"给内存数据上保险",不是数据正确性必需品 —— 这正是它和 MySQL/ES 最大的不同。
SAVE(阻塞)/BGSAVE(后台):fork 子进程,利用 COW(写时复制)把当前全量数据写成紧凑二进制文件 dump.rdb。优点:文件小、恢复快、适合备份。缺点:两次快照之间崩溃,期间写入全部丢失。
自动触发:save 3600 1 / 300 100 / 60 10000(N 秒内 ≥M 次写)等配置;主从全量同步也传 RDB。
把每个写命令按 RESP 协议追加到文件末尾(appendonly.aof),重启时逐条重放重建数据。丢失窗口由 fsync 策略决定,可做到接近零丢失。缺点:文件大、恢复慢(重放全部命令)。
这就是"Redis 的 AOF 像 WAL"说法的来源 —— 但见 3.4 的本质区别。
appendfsync | 行为 | 崩溃丢数据窗口 | 适用 |
|---|---|---|---|
always | 每个写命令都 fsync | 几乎不丢(最多一条命令) | 强一致场景,吞吐最低 |
everysec(默认) | 每秒批量 fsync 一次 | 最多丢 1 秒写入 | 绝大多数生产场景 |
no | 交给 OS 决定何时刷 | 不定,可能丢较多 | 吞吐优先、可容忍丢失 |
注意"写 AOF buffer"与"fsync 到磁盘"是两件事:always 是每条命令都等落盘确认,最稳也最慢;everysec 用后台线程刷,兼顾吞吐与 ≤1s 丢失。
AOF 只增不改,跑久了会巨大且充满可抵消的命令(如对同一 key 的多次 set)。重写不是"整理旧文件",而是基于当前内存里的最新数据,生成一份能达到同样状态的最小命令集,写进新文件后原子替换。触发条件:文件超过上次 rewrite 后的一倍且 ≥64MB(auto-aof-rewrite-percentage / min-size)或手动 BGREWRITEAOF。
重写期间的阻塞点:fork 瞬间(大实例可能毫秒~百毫秒级,与内存页数相关);always 模式下父进程每次写命令还要 fsync 旧 AOF,会加剧卡顿 —— 生产多用 everysec。
数据本体在磁盘。redo 是正确性部件:没有它,崩溃后磁盘数据可能不完整。日志记录的是"对磁盘数据的修改",重放后数据页回到正确状态,日志可以循环覆盖。
数据本体在内存。不开持久化 Redis 照常运行,只是重启归零。AOF 记录的是"重建内存的命令流"(是数据本身的一种序列化),不是对某个落盘结构的增量修改。所以它必须全量保留(直到 rewrite 压缩),语义更接近 MySQL 的 binlog 而非 redo。
aof-use-rdb-preamble yes,默认开):AOF 文件头先放一份 RDB 二进制快照(快速加载),尾部接快照之后的增量命令(不丢细节)。重启加载从"重放全部命令"变成"读快照 + 重放增量",速度和数据完整度兼得。base(RDB 格式主文件)+ 多个 incr(增量)+ manifest(清单)管理,rewrite 生成新 base;对比 4.0 之前"单个大 AOF"更利于故障恢复与备份。启动时若 AOF 开启,优先用 AOF 恢复(比 RDB 更新);aof-load-truncated 允许在 AOF 尾部残缺(如断电半条命令)时自动截断并继续启动 —— 面试可提"生产上应先备份再启动,避免截断毁掉可修复数据"。
用 AOF:因为它记录到更近的时间点,数据更全(尤其 everysec 只丢 ≤1s,而 RDB 可能丢数分钟)。4.0+ 混合格式下 AOF 头本身是 RDB,加载也快。Redis 官方默认配置两者都生成:AOF 保数据新鲜度,RDB 保快速恢复与备份/主从同步。
fork 由主线程执行,成本与进程内存页数相关,大实例 fork 期间会有毫秒到百毫秒级停顿(fork 耗时 指标可见);fork 后子进程独立跑,不阻塞。COW 期间父进程每改一个内存页就要复制该页,写密集时内存会短暂上涨(约等于写集大小),需给实例留内存余量,别让 maxmemory 顶满导致 fork 失败或触发淘汰。这是"Redis 单线程 + 持久化"最常见的生产坑。
全量同步 = 主库生成 RDB(磁盘版或 diskless 直传)给从库,之后主库把增量写命令通过 replication stream 推给从库继续追赶。生产建议:主库至少开 AOF(everysec),从库可按需开;若主库完全关持久化且重启,可能以空数据状态服务,触发从库也清空重同步(有保护机制但风险仍在)——所以"主库持久化不能省"。从库可开 RDB 做备份,降低对主库的 bgsave 压力。
纯缓存(可接受全量重建)可以关掉 AOF 甚至全关,省掉 fork/刷盘开销;但不能接受的场景是"缓存里存了可重建代价高的数据或短暂当存储用"。面试加分表述:持久化开关取决于数据重建成本 × 丢失容忍度,Redis 本身不保证持久性语义,需要时自己开、自己测恢复演练。
ES 面试最爱问的两个词:refresh 与 flush 的区别;以及为什么 ES 是"近实时"的。底层是 Lucene。
refresh 管"可见性",flush 管"持久性" —— 这是全篇第二个核心口诀。refresh 前数据在内存 buffer,此时 搜不到,但已被 translog 保护;refresh 后能搜到,但若节点宕机且未 flush,磁盘上还没有它 —— 仍然靠 translog 恢复。
Lucene 的存储单元,不可变(写后不修改)。一次 refresh 把 buffer 固化成 1~N 个新段;查询扫全部分段再合并结果。不可变带来:无需加锁、可被 OS page cache 缓存、利于并发与压缩。
ES 的 WAL:记录每个写操作的完整数据,默认 request 模式每次写请求后 fsync(可改 async,每 5s 刷)。作用是保证 buffer/新段里"还没真正落盘"的数据在崩溃后可重放。
flush 时写下的"检查点"文件,列出磁盘上所有已确认的 segment。崩溃恢复:从最近 commit point 载入段 → 重放其后 translog → 数据完整。
实时可见 = 每条写入都建段并 fsync,但建大量小段 + 频繁 fsync 会让写入吞吐崩掉。于是 ES 用 index.refresh_interval(默认 1s)攒批建段 —— 写入确认很快(只要 translog 落盘),但最多延迟 1s 才可搜。想要更快可见可调小 interval 或调 refresh=true,代价是段更多、合并压力更大(大促/日志场景常临时调大或关掉 refresh 换吞吐)。
.del 标记。_forcemerge 可手动触发)。单副本下:节点宕机,未 flush 但已 fsync 的 translog 在磁盘上 → 重启后 commit point + translog 重放,默认 request 模式理论上不丢已确认的写入。多副本下由主分片负责 translog 同步/重放,副本从主同步数据。async 模式则最多丢约 5s(取决于 sync_interval 与数据量)。
refresh:把内存 buffer 生成新 segment 并打开搜索视图,让新数据可被搜到,但不 fsync(数据在 OS page cache,断电会没),默认 1s 一次,可手动 POST /_refresh。flush:执行 Lucene commit —— fsync 所有段到磁盘 + 写 commit point + 清空 translog,是真正的持久化动作,默认 30min 或 translog 达 512MB 自动触发(index.translog.flush_threshold_size)。一句话:refresh 管可见、flush 管持久;refresh 频繁则段碎,flush 由 translog 阈值兜底。
默认 index.translog.durability=request:主分片确认写入前 translog 已 fsync,节点进程崩溃/断电后靠 commit point + translog 重放,已确认的写入不丢(前提是磁盘本身没坏、没做 forcemerge 之类删 translog 的操作)。改成 async(每 5s 刷)会提高吞吐,但节点宕机可能丢最近几秒已确认的写入。真正会"丢"的常见场景:副本数为 0 + 分片所在的机器磁盘损坏、或集群脑裂后主分片数据被旧副本覆盖(需要 quorum/避免 split brain)。
收益:无锁并发读、可整段压缩、可整体进 page cache、崩溃恢复只需按段粒度校验。问题:更新/删除只能标记后靠 merge 清理(写入放大)、小段多了查询要合并很多结果集(段数量与查询延迟直接相关),所以 merge 策略(tiered:按大小分层合并)与 refresh 频率要一起调优。
定位相同:都是"先于主数据落盘、用于崩溃后重放丢失部分"的 WAL。差异:① redo 记录页级物理修改、循环覆盖、只服务 InnoDB 恢复;translog 记录文档级操作数据(近似逻辑日志)、随 flush 清空、主要覆盖 refresh 产生的"可搜但未落盘"窗口。② redo 的提交确认和 fsync 是事务语义的一部分;ES 的写入确认绑定 translog fsync(request 模式),与 refresh 解耦。③ MySQL 没有"内存 buffer 中可见性延迟"概念(提交即可见),ES 有 refresh 这一层 —— 因为 ES 的"读"是搜索索引结构,需要先固化到段才能高效查。
| 维度 | MySQL / InnoDB | Redis | Elasticsearch |
|---|---|---|---|
| 数据本体在哪 | 磁盘(16KB 数据页),Buffer Pool 只做缓存 | 内存,磁盘只是备份/恢复源 | 磁盘(segment),但写入先经内存 buffer + page cache |
| 持久化的性质 | 正确性必需(WAL) | 可选保险(纯缓存可全关) | 正确性必需(translog 兜底 refresh 空窗) |
| "先写日志"是哪个 | redo log(物理逻辑、循环写、组提交) | AOF(命令流、纯追加、rewrite 压缩) | translog(操作数据、随 flush 清空) |
| 第二份日志 | binlog(逻辑,复制/PITR,两阶段提交对齐) | RDB 快照(含混合持久化 AOF 头) | Lucene commit point(落盘检查点) |
| 日志内容粒度 | 页号 + 页内偏移的修改 | 完整写命令(RESP) | 完整文档操作 |
| 默认丢失窗口 | ≈0(参数=1 每次提交 fsync) | ≤1s(everysec)或 ≈0(always) | ≈0(request 模式)或 ≤5s(async) |
| 数据何时对读可见 | 事务提交即对其它事务可见(隔离级别内) | 写命令执行后立即可见 | refresh 后(默认 ≤1s 延迟,近实时) |
| 快照/合并机制 | checkpoint(推进 LSN、回收 redo) | bgsave/RDB + AOF rewrite | flush(fsync+commit point)+ segment merge |
| 崩溃恢复 | checkpoint 起重放 redo → 对照 binlog 补/回滚 → undo 回滚未提交事务 | AOF 优先重放(4.0+ 混合格式先载 RDB 头) | commit point 载入段 → 重放 translog |
| 最大性能杀手 | 随机刷脏页、redo 写满强制 checkpoint | fork 大内存停顿、always 模式每条 fsync | refresh 过频段碎裂、merge 追赶不上 |
凡是把"写"的确认建立在顺序日志上、把"数据本体"的落盘异步化的系统,都遵循 WAL 骨架;差别只在于 ① 数据本体在哪(决定日志是不是必需品)、② 日志记什么(决定重放/复制能力)、③ 读的可见性由哪一层控制(提交 / 直接生效 / refresh)。MySQL 三份日志分层最全,Redis 用快照+命令流给自己上保险,ES 用 translog 给"近实时可见"的缓冲窗口兜底 —— 各为各的数据模型服务。
模拟面试节奏:每题先自己讲 30~60 秒,讲完再展开答案,看漏了哪个点。
提交前 redo 已落盘(组提交合并 fsync)。崩溃后 Buffer Pool 里的脏页没了,但磁盘数据页还是旧值 → 启动时从 checkpoint LSN 开始重放 redo,把"已提交事务写过的页"重新改到最新。整个过程对客户端透明:确认提交的那一刻,数据就已经被日志"钉"在磁盘上了。
职责不同:redo 保本机崩溃恢复(引擎层、物理、循环),binlog 保复制与时间点恢复(Server 层、逻辑、追加归档)。因为两份日志独立落盘,不原子,可能崩在中间导致主库与从库对"这个事务提交没有"判断不一致 → prepare 先让 redo 记住事务(含 XID),binlog 落盘成功才 commit;恢复时见 prepare 不见 binlog 的补提交(binlog 有就都执行)、只见 prepare 不见 binlog 的回滚。本质是分布式事务思想用在两个本地组件之间。
① 定位页:按聚簇索引 B+ 树找到数据页,不在 Buffer Pool 则读盘。② 写 undo:保存旧值(回滚 + MVCC 链)。③ 改内存页并记 redo(物理逻辑变更)。④ 事务提交:redo 落盘(1 则 fsync,可组提交)→ 写 binlog 落盘 → redo 打 commit 标记(两阶段)。⑤ 异步:checkpoint 把脏页刷回磁盘;若页被部分写坏,doublewrite 提供整页副本。⑥ 老版本 undo 由 purge 清理。这一条线能串完 InnoDB 持久化全部考点。
AOF 开启则只加载 AOF(数据更新);没开 AOF 才读 RDB。混合格式下 AOF 文件 = RDB 二进制头(直接载入内存,秒级)+ 尾部增量命令(重放),加载远快于纯命令流,又不丢 rewrite 之后的细节。7.0 multi-part 则是按 manifest 依次加载 base + incr。
MySQL:redo 落盘频率(靠组提交与参数 0/1/2 权衡)与磁盘 IOPS;脏页刷盘跟不上会写满 redo 强制 checkpoint。Redis:单线程 CPU 与 fork(持久化瞬间停顿、COW 内存放大),always 时 fsync 直接卡主线程。ES:translog fsync 频率(request 模式每请求一次)→ 用 bulk 批量写摊薄;refresh 每 1s 固化 buffer,段数增长 → merge 线程成为后台瓶颈。三者共同解法都是:批量、攒批、把 fsync 从热路径上摘掉或合并。
先给判断:形式上像 WAL(先落命令日志、崩溃重放),语义上更接近 binlog。WAL 的经典定义是"先于数据页修改写日志、日志服务于本机崩溃恢复、数据本体在磁盘";Redis 数据本体在内存,AOF 是数据的完整序列化(备份/重建用),不是对磁盘结构的增量。所以可以说"Redis 用 AOF 实现了 WAL 式的先写日志思想,但它的角色是持久化备份,不像 InnoDB redo 那样是正确性必需"。能分清楚这一层,说明真懂。
取决于 translog 模式:request(默认)下写入成功 = translog 已 fsync,即使 node 立刻宕机,重启后也能从 commit point + translog 找回 —— 数据是"安全的",但不在磁盘的 segment 上(可能还在内存 buffer 或 page cache 段里)。async 模式则成功 ≠ 落盘,最多丢几秒。另外注意"落盘安全"≠"可搜索":要等下次 refresh。三层解耦:translog 管不丢、refresh 管可见、flush 管最终落盘。
第一轮只记三句话:MySQL = 改页前先写 redo(binlog 是复制用的另一份);Redis = 内存是本体,RDB 快照 + AOF 命令流二选一或混合;ES = refresh 管可见、flush 管持久、translog 兜底。第二轮按第五节表格逐格问自己"为什么"。第三轮用第七节问答做口述演练。祝面试顺利。