Theory · MySQL · Crash-safe

三大日志与 crash-safe

redo 管崩溃恢复、undo 管回滚与 MVCC、binlog 管复制与按时间点恢复 —— 两阶段提交把它们焊成一个整体

redo log · InnoDB 层

物理日志,WAL 先写日志再写页;循环写,掉电后前滚恢复——crash-safe 的根基

undo log · InnoDB 层

逻辑日志,记录反向操作;事务回滚 + MVCC 版本链(细节见 MVCC deck)

binlog · Server 层

逻辑日志,所有引擎共用、追加写;主从复制与按时间点恢复的唯一事实来源

三大日志是 MySQL 事务必考题的另一个高峰,和锁、MVCC 并列。这份 deck 的主线:先分层看清谁属于谁,然后逐个拆 redo、undo、binlog,最后用两阶段提交和崩溃恢复把三者串成一个整体,再用一条 UPDATE 全流程收尾。所有参数默认值和版本变更都对照 8.0 手册核实过,8.0.30 的 redo 容量参数、binlog_format 弃用这类版本点都在对应页注明。

Why Logs Matter

先看现象:用户看到"支付成功",重启后这笔钱却不见了

-- 用户支付 100 元,账户余额从 500 改成 400
BEGIN;
UPDATE accounts SET balance = 400 WHERE id = 1;
COMMIT;          -- 客户端收到:提交成功 ✓

-- COMMIT 返回后,这一行其实还在内存的 buffer pool 里,
-- 磁盘上的 accounts.ibd 仍然是 500。
-- 数据页要等后台线程"慢慢"刷回磁盘。

-- 0.3 秒后:机房断电 —— 内存全丢。

-- 重启后余额是多少?
--   没有 redo log  → 400 这个改动凭空消失,余额变回 500。
--                     用户扣款成功了,商家没收到钱,账对不上。
--   有 redo log    → 400 完好无损。这就是 crash-safe。
关键观察:"已经返回成功"和"已经写进磁盘"是两件事。数据库的做法是承认这个差距,但用一份额外的日志把它补上——先保证"怎么重做"落了盘,数据页晚点写也无妨。

为什么不能"提交时就把数据页写回磁盘"?

因为太慢。数据库按 16KB 的读写磁盘,改一个字节也要把整页写回;而且一个事务改的行往往散落在不同页、页又散落在文件各处——等于每次提交都要做好几次随机写。机械盘一次随机写约 10ms,一秒只能提交一百来个事务。

那么"日志"换掉了什么?

它把随机写换成顺序写:提交时不碰数据页,只在日志文件末尾追加一条"把第 N 页的某处改成 X"的记录并落盘。顺序追加比随机写快一到两个数量级。这就是 WAL(Write-Ahead Logging,预写日志):先写日志,再(慢慢)写数据

但一份日志不够,要三份

redo:管"崩了之后把做过的事重放一遍"——解决上面的断电丢数据;
undo:管"把没做完的事撤销回去"——断电时可能有事务只改了一半;
binlog:管"把发生过的事告诉别人"——从库要照着抄,出事要按时间点恢复。
三份日志分属两层、各管一段,最后还要用两阶段提交把它们焊成一个整体。

先建立一个直觉:日志就是飞机的黑匣子 + 录像带。redo 是"按录像把操作重做一遍",undo 是"按录像倒带",binlog 是"给别人的一份完整录像副本"。三者的全部区别,只在于给谁看、用来往前还是往后
动机页:先给一个能想象出后果的具体事故(COMMIT 已返回但数据页还在内存,断电后改动丢失),再说明"提交时刷数据页"为什么不可行(16KB 页 + 随机写,机械盘约 10ms 一次),从而引出 WAL 的取舍:用顺序追加的小日志换随机写的大页。右侧顺势列出三份日志各自管的那一段(重做 / 撤销 / 告诉别人),为后续三页分工做铺垫。末尾用"黑匣子 + 录像带"类比建立最小直觉。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(先记住这个,细节后面展开)
页(Page)InnoDB 读写磁盘的最小单位,默认 16KB;改一个字节也要整页写回
buffer pool数据页在内存里的缓存区;事务改的是内存里的页,磁盘上的副本是旧的
脏页(Dirty Page)在内存里被改过、还没刷回磁盘的页
刷盘 / fsync把内存里的数据真正交给磁盘。write 只是交给操作系统缓存,fsync 才保证断电不丢
WAL预写日志:修改数据之前先把"改了什么"写进日志——用顺序写换随机写
物理日志记"哪个页的哪个位置改成了什么",重放时不用解析 SQL(redo 属于此类)
逻辑日志记"做了什么操作",如 UPDATE t SET c=1 WHERE id=2(binlog / undo 属于此类)
LSNLog Sequence Number,redo 的全局递增坐标:写到哪、刷到哪、页改到哪,全用它对齐
checkpoint一个界线:它之前的 redo 对应的脏页已经刷盘,那部分日志可以被覆盖复用
两阶段提交 2PC让 redo 与 binlog 要么都生效、要么都不生效的跨层提交协议(prepare → binlog → commit)

需要但不在这份 deck 里的前置

transaction-basics.html → ACID 各自靠什么实现、事务的生命周期与长事务治理
mvcc.html → undo 版本链、ReadView 与 purge(本 deck 只讲 undo 在回滚里的角色)

本 deck 怎么用这些词

为了不纠结名词,后面统一按"这份日志写给谁看、往哪个方向用"来区分:redo 向前重做、undo 向后撤销、binlog 给别人抄。遇到 LSN 就当它是"日志的页码"。

最小心智模型(一句话统摄全文):
三份日志解决的其实是同一个矛盾:内存快但一断电就丢,磁盘稳但随机写太慢

做法是先在磁盘上顺序记一笔,再慢慢改数据页;崩溃后拿这笔记录决定"重做还是撤销"。后面所有机制——循环写、checkpoint、刷盘三档、组提交、两阶段提交——都是这个矛盾的工程解。
阅读提示:术语不用背,遇到忘了的回来查这一页。真正要背的只有"双 1"、三个崩溃点的下落,和最后那张速查表。
前置页:挑本 deck 真正会用到的十个术语,重点区分物理日志 vs 逻辑日志(这是"redo 不能被 binlog 取代"的推理起点)与 write vs fsync(这是"刷盘三档"语义的前提)。两条外链前置覆盖 ACID 与 MVCC。最小心智模型把三份日志统一到"内存快但丢、磁盘稳但慢"这一个矛盾上,后文所有机制都是它的工程解。

Overview · Three Logs, Two Layers

全景:binlog 在 Server 层,redo / undo 在 InnoDB 层

开场那笔"支付成功却丢了的钱",靠 redo 补回来、靠 undo 撤销做一半的、靠 binlog 通知从库。先看三份日志分别住在哪一层。

三大日志分层全景:Server 层 binlog 与 InnoDB 层 redo/undo 及两阶段提交顺序 binlog 属于 Server 层、所有引擎共用;redo 与 undo 属于 InnoDB 层。事务提交按 redo prepare、写 binlog、redo commit 的顺序跨层执行;binlog 由 dump 线程推给从库。 Server 层 binlog(二进制日志) 所有引擎共用 · 逻辑日志 · 追加写 binlog.000001 → binlog.000002 写满一个文件换下一个 用途: ① 主从复制(dump → 从库) ② 按时间点恢复 PITR 默认格式 ROW(8.0) 事务提交 · 内部两阶段提交顺序 ① redo prepare InnoDB 写 redo 并落盘 ② binlog 写入 write + fsync(sync_binlog) ③ redo commit InnoDB 写提交标记 InnoDB 层(存储引擎) redo log(重做日志) 物理日志 · 循环写 #innodb_redo / #ib_redoN 崩溃恢复:前滚已提交事务 仅 InnoDB 有 · 保证 crash-safe undo log(回滚日志) 逻辑日志 · 反向操作记录 rollback segment / undo 表空间 回滚未提交事务 + MVCC 版本链 版本链细节见 MVCC deck 谁属于谁(易错点) MyISAM 没有 redo/undo 也不 crash-safe, 但照样有 binlog——binlog 是 Server 层的, 有 binlog ≠ 崩溃恢复安全。 三种日志一句话 redo:重做页(物理);undo:撤销行(逻辑); binlog:记录变更事实(逻辑),复制与 PITR 只认 binlog。 原子性 / 持久性分工 原子性靠 undo(回滚),持久性靠 redo(前滚), 两者都是 InnoDB 的;主从一致靠 binlog。 binlog → 从库 binlog 写完后由 dump 线程推给从库 IO 线程, 写入 relay log 回放——这是下一站: replication deck(复制与高可用)
这页先建立坐标系:binlog 是 Server 层的,所有引擎共用,所以 MyISAM 也有 binlog;redo 和 undo 是 InnoDB 的,服务于事务本身。中间一条横带是提交顺序:先 redo prepare,再写 binlog,最后 redo commit——这条顺序线是本 deck 后半场的核心,第 11 页会画完整时序。右下角提示 binlog 会流向从库,与复制 deck 打通。答题时先说分层,再说用途,层次立刻清晰。

Redo Log · WAL · §17.6.5

redo log:WAL 思想——随机写换顺序写

没有 WAL 的世界

每次事务提交都要把改过的数据页(16KB)刷回磁盘。一个页只要改了几个字节也要整页写,且不同页散落在表空间各处——大量随机 IO,提交延迟完全被磁盘性能卡死。

WAL:Write-Ahead Logging

提交时只把"改了什么"先顺序追加到 redo log 并落盘,数据页留在 buffer pool 里慢慢刷。崩溃后按 redo 重放(前滚),把没来得及刷的页恢复出来——顺序写小日志换随机写大页

要素说明
physiological loggingredo 是"物理到页、逻辑到行"的日志:记录哪个表空间哪一页、哪个偏移、做了什么修改(page-oriented physical redo)。重放不需要解析 SQL,恢复极快
log buffer先写内存中的 log buffer(innodb_log_buffer_size 默认 16MB),按策略刷到 redo 文件(默认在 #innodb_redo 目录)
LSN(log sequence number)redo 的全局单调递增坐标:写入位置、checkpoint 位置、页上的最新修改都拿 LSN 说话
为什么必须是物理日志崩溃恢复要重放到"崩溃那一刻的页状态",逻辑日志(binlog)缺少页级信息、也无法驱动 checkpoint——这是 redo 不能被 binlog 取代的根本原因
答题句式:"redo log 的本质是 WAL:为了把提交时的随机页写推迟、用顺序日志先落地。物理日志的代价是对'页物理损坏'无能为力——所以 InnoDB 还需要 doublewrite 机制保护页写入。"
讲 redo 先讲 WAL 解决什么问题:提交时刷随机数据页太慢,WAL 改成先把修改记录顺序写进日志。redo 是 physiological logging,精确到页内偏移,恢复时重放即可,不用解析 SQL。LSN 是 redo 的坐标系,后面 checkpoint 和恢复都靠它。最后埋一个伏笔:正因为它只管"把日志重放到页上",页本身写坏它救不了,这就引出 buffer-pool deck 里的 doublewrite。

Redo · Circular Write · Checkpoint

循环写:write pos 追 checkpoint,追上就得停

redo log 循环写:write pos 与 checkpoint 之间的环形空间 redo 容量构成一个环,write pos 顺时针前进写入新 redo;checkpoint 顺时针推进表示之前的 redo 对应脏页已刷盘、空间可覆盖;write pos 与 checkpoint 之间是已写入但尚未 checkpoint 的部分,write pos 追上 checkpoint 时必须先刷脏推进 checkpoint。 checkpoint write pos 已写 · 未 checkpoint 已 checkpoint · 可覆盖空间 redo log 环形空间 容量 = innodb_redo_log_capacity 8.0.30+ · 默认 100MB ① write pos:顺时针前进,写新 redo 写满一个文件换下一个,绕环循环; 8.0.30 起约 32 个 #ib_redoN 等分容量。 ② write pos 追上 checkpoint → 整库降速 必须先推进 checkpoint(把对应脏页刷盘) 才能腾出新空间——所有更新被阻塞在刷脏上。 ③ checkpoint LSN 存在 redo 文件头 崩溃恢复从最新 checkpoint LSN 起重放(§17.6.5); 容量越大,checkpoint 可以越"懒"、刷脏越平滑。
这张环形图要会讲:蓝色弧段是已写入但还没 checkpoint 的 redo,checkpoint 顺时针推进表示它后面的空间可以覆盖复用。两个指针之间的距离就是"最多能积累多少未刷脏数据"。write pos 追上 checkpoint 时系统必须停下来刷脏,这就是后面"抖动"问题的根源。版本点:8.0.30 起 redo 容量由 innodb_redo_log_capacity 控制,默认 100MB,落地在 #innodb_redo 目录下约 32 个文件,取代旧的 log_file_size 乘 files_in_group。

Redo · Durability Knob

innodb_flush_log_at_trx_commit:持久性的三档开关

取值提交时做什么丢数据窗口适用
0提交不写 log buffer;后台线程每秒 write + fsync 一次mysqld 崩溃 / 主机断电都丢约 1 秒事务纯测试环境
1(默认)每次提交:log buffer → redo 文件,并 fsync 到磁盘不丢已确认事务(除磁盘本身损坏)生产默认,配合 sync_binlog=1 即"双 1"
2每次提交:写到 redo 文件的 OS page cache;每秒 fsync 一次mysqld 崩溃不丢(数据还在 OS 缓存);主机断电 / OS 崩溃丢约 1 秒容忍秒级丢失换性能(如日志型写入)

"双 1"配置

innodb_flush_log_at_trx_commit=1 + sync_binlog=1。两者分别管 redo 和 binlog 的落盘时机,缺一个就有一份日志可能丢——答题时要把两条一起报出来,并说明各自防的是哪种崩溃。

fsync 太贵 → 组提交

三档的代价都来自 fsync。InnoDB 与 binlog 都支持组提交:同一时刻并发提交的多个事务合并成一批,一次 write、一次 fsync 服务整组(redo 组提交 + binlog 组提交,见第 13 页)。

追问预警:"设成 2 为什么 MySQL 重启不丢、机器断电才丢?"——因为 2 的 write 已到达 OS page cache,mysqld 进程死了数据仍在内核里;断电时内核缓存没了,丢失的是最近一秒还没 fsync 的事务。
三档开关是必考参数。关键在区分两种崩溃:mysqld 进程崩溃和主机掉电。档 0 两种都丢一秒;档 2 写到了 OS 缓存,进程崩溃不丢、掉电丢一秒;档 1 每次提交 fsync,不丢。双 1 指这一档加 sync_binlog=1, redo 和 binlog 各自落盘,缺一不可。性能话题顺势引出组提交:fsync 是瓶颈,合并提交才治本。

Undo Log · §17.6.6

undo log:回滚与 MVCC 的共同底座

undo 分类服务谁何时可丢弃
insert undo log仅事务回滚(没有其他视图会读插入的新版本)事务提交即可丢弃(commit 即可清)
update undo log回滚 + MVCC 版本链(一致性读要沿它找旧版本)等"没有任何 ReadView 还可能需要它"——由 purge 判定

记录内容:反向操作

insert 反向是 delete、update 反向是"把旧值写回"、delete 反向是"把整行插回"。所以回滚 = 沿反向操作执行一遍;而"把旧值写回"恰好也是快照读要的历史版本——回滚与 MVCC 共用 update undo。

8.0 的 undo 表空间

undo 从系统表空间拆为独立 undo 表空间(默认 2 个),支持 innodb_undo_log_truncate 自动截断回收——长事务钉住 update undo 导致 undo 膨胀的问题依旧存在(见 MVCC deck 的 purge 与长事务页)。

本 deck 视角:在崩溃恢复里,undo 的角色是"回滚未提交事务"——redo 前滚把数据库推回崩溃那一刻(可能包含未提交的修改),再由 undo 把未提交事务逐个撤销(见第 14 页)。MVCC 版本链、ReadView、purge 的完整机制见 MVCC deck,此处不重复。
undo 的核心是分类:insert undo 提交即丢,update undo 要服务版本链、等 purge 判定才能清。记录的是反向操作,回滚就是把反向操作执行一遍,而反向操作恰好就是历史版本,所以回滚和 MVCC 共用 update undo。这页只讲 undo 在崩溃恢复里的角色——前滚之后的回滚;版本链、ReadView、purge 的细节全部指向 mvcc.html,两边分工不重复。

Binary Log · Formats · §19.2.1

binlog 三种格式:statement / row / mixed

格式记录什么体积 / 开销主从一致性风险
STATEMENT记录 SQL 语句原文(及上下文信息)最小;一个语句影响百万行也只记一条不确定语句在从库重放可能得到不同结果(RAND/UUID/LIMIT 等,见 复制 deck
ROW(8.0 默认)记录行的实际变更(哪一行、改成什么样),binlog_row_image=FULL 默认记整行前后镜像最大;按行放大写放大确定性强,复制最安全;更新范围可精确回放
MIXED默认 statement,遇到不安全语句自动切 row介于两者折中;但"何时切换"的边界依赖实现判断

RC 只允许 row

READ COMMITTED 下 gap 锁禁用、加锁范围不受控,statement 重放结果可能不一致——手册直接规定 RC "only row-based binary logging is supported";MIXED 在 RC 下自动按 row 记录(呼应 MVCC deck 第 15 页)。

版本口径

8.0 起 binlog_format 默认 ROW;手册 §19.2.1 注明该设置自 8.0.34 起被弃用——官方方向是 row-only。答"选什么格式"时补一句版本趋势是加分项。

对比锚点:row 记录的是"结果",statement 记录的是"过程"。复制要求可重放的事实,所以默认与趋势都指向 row;statement 的价值只剩日志体积,而它带来的不确定性远大于体积收益。
三格式对比表是复制题的地基。statement 记语句,体积小但不确定语句会翻车;row 记行变更,体积大但确定,8.0 起默认就是 row;mixed 折中,遇到不安全语句自动切 row。两个硬结论:RC 下只允许 row,因为 gap 锁禁用后语句重放结果不可控;binlog_format 这个设置本身从 8.0.34 开始弃用,说明官方在走向 row-only。具体陷阱例子留到复制 deck 展开。

Binary Log · sync_binlog · Redo vs Binlog

binlog 的落盘开关,以及与 redo 的四维对比

sync_binlog提交时行为风险
1(8.0 默认)每次提交 write + fsync binlog不丢已确认事务(最安全)
0write 交给 OS,fsync 由 OS 决定主机崩溃丢 OS 缓存里的 binlog
N攒 N 个事务组提交 fsync 一次崩溃最多丢 N−1 个事务的 binlog
用途只有两个:①主从复制(dump 线程推给从库)②按时间点恢复 PITR(全量备份 + 从备份点起重放 binlog)。binlog 不参与崩溃恢复的"前滚"——那是 redo 的活。
维度redo logbinlog
所属层InnoDB 引擎层Server 层(所有引擎)
内容类型物理日志(页级修改)逻辑日志(语句 / 行变更)
写方式循环写,空间固定复用追加写,写满换文件不覆盖
核心用途崩溃恢复(前滚)复制 + 按时间点恢复

循环写与追加写的差异最常被追问:redo 敢覆盖是因为旧 redo 对应的脏页必然已刷盘(checkpoint 保证);binlog 是复制与恢复的事实来源,永远不能覆盖。

sync_binlog 是 binlog 侧的持久性开关,默认 1,和 trx_commit 凑成双 1;设 N 是用丢 N 个事务换 fsync 次数。右边四维对比是高频题:层次、物理还是逻辑、循环写还是追加写、用途。追加写要主动解释:redo 敢循环写靠 checkpoint 保证旧日志对应的页已刷盘,binlog 是复制和 PITR 的事实来源,绝对不能覆盖。

Internal 2PC · The Core Slide

两阶段提交:redo prepare → 写 binlog → redo commit

MySQL 内部两阶段提交时序与两个崩溃点 提交事务先在 InnoDB 写 redo 并进入 prepare 状态;随后 Server 层将 binlog 写入并 fsync;最后 InnoDB 写 redo commit 标记。崩溃点 A 位于 binlog 写入前,恢复时回滚;崩溃点 B 位于 binlog 落盘后,恢复时凭完整 XID 对账提交。 InnoDB · redo log 侧 ① 写 redo · 进入 PREPARE undo 记录 + 页修改 + redo(含 XID) trx_commit=1 时 fsync 落盘 ③ 写 redo · COMMIT 标记 将事务从 prepare 翻转为 committed 之后释放锁、清理 undo(commit 阶段) Server 层 · binlog 侧 ② 写 binlog + fsync binlog cache → binlog 文件(sync_binlog=1 时 fsync) prepare 完成 binlog 落盘 崩溃点 A 崩溃点 B 崩溃恢复对账规则 恢复时对每个 redo 处于 prepare 的事务:拿 XID 去 binlog 对账——binlog 中存在完整 XID event → 提交(B 点);不存在 → 回滚(A 点)
这是本 deck 的核心页。提交事务分三步:InnoDB 先写 redo 进入 prepare,Server 层再把 binlog 写入并 fsync,最后 InnoDB 写 commit 标记。两条虚线是崩溃点:A 点在 binlog 之前,恢复时 binlog 里没有这个事务,回滚;B 点在 binlog 之后,虽然 commit 标记没写,但恢复时拿 XID 去 binlog 对账,有完整 XID event 就补提交。把这两条虚线讲清楚,两阶段提交就通了。

Proof by Contradiction

反证法:去掉任一环节,崩溃就是不一致

方案崩溃点恢复结果后果
不用 2PC:
先 redo commit,后写 binlog
redo 已提交、binlog 还没写主库恢复后有该事务;binlog 里没有 → 从库不会执行主库有、从库无——主从不一致,且无 binlog 可补
不用 2PC:
先写 binlog,后 redo commit
binlog 已写、事务没提交主库恢复时回滚该事务;binlog 里却有 → 从库照常执行主库无、从库有——从库凭空多数据,且是"已回滚"的脏事务
2PC(真实做法)任意点prepare 事务拿 XID 对账 binlog:有 → 补提交;无 → 回滚主库恢复结果与 binlog 始终一致 → 主从一致、crash-safe

为什么用 binlog 当裁决者

binlog 是复制与 PITR 的唯一事实来源:binlog 里没有的事务,全世界的从库和备份都不会有。让"是否进入 binlog"决定主库恢复时的生死,主库状态就永远向 binlog 对齐——这是把两份日志焊死的锚点。

XID 怎么串起两份日志

redo 的 prepare 记录携带事务的 XID,binlog 事务末尾也写 XID event。恢复时扫 redo 发现 prepare 状态事务 → 拿 XID 查 binlog 是否有完整 event → 有则提交、无则回滚。一组 ID、两份日志、一个裁决。

答题句式:"两阶段提交解决的是 redo 和 binlog '两份日志写一半'的一致性问题:prepare 让 InnoDB 先表态但不定案,binlog 落盘作为定案依据,commit 标记可以晚点补——恢复时谁有 XID 谁说了算。"
反证法是讲透 2PC 的最好方式。先 redo 后 binlog,崩在中间主库有从库无;先 binlog 后 redo commit,崩在中间从库有主库无——两个方向都是主从不一致。2PC 的巧思在于把 binlog 当裁决者:binlog 是复制和恢复的唯一事实来源,用 XID 对账让主库恢复结果永远向 binlog 对齐。答题时把两个反例推演出来,比背定义有说服力得多。

Group Commit · FLUSH / SYNC / COMMIT

binlog 组提交:三阶段队列摊薄 fsync

阶段做的事
FLUSH各事务的 binlog cache 内容写入公共 binlog 文件(write 到 OS cache);组内 leader 带队,后来者挂到队列等下一轮
SYNC对整个组做一次 fsync——N 个事务的持久化成本摊成 1 次;可用 binlog_group_commit_sync_delay/_no_delay_count 故意等一小会儿凑更大的组
COMMIT组内事务逐一进入 InnoDB commit(写 redo commit 标记、释放锁);binlog_order_commits 保证提交顺序与 binlog 一致

三阶段命名出自官方 Worklog WL#5223 与手册 §19.1.6.4(Binary Logging Options and Variables)

与 redo 组提交的接力

完整提交路径是:redo prepare(InnoDB 组提交 fsync)→ binlog FLUSH → SYNC → COMMIT。两段组提交各自合并 fsync:redo 一次、binlog 一次——双 1 配置下高并发 TPS 依然可观的原因就在此。

为什么组提交同时帮了复制

同一组事务的 XID 在 binlog 中连续落盘,从库并行回放(WRITESET 等)也更容易找依赖组——组提交是"主库省 fsync、从库好并行"的公共地基(见 复制 deck)。

延迟参数提醒:sync_delay 是"为了凑组故意等的微秒数",是拿单笔延迟换吞吐的旋钮;低并发时它只会白等,高并发时收益明显。
binlog 组提交分三个阶段:flush 把各事务的 binlog cache 写进公共文件,sync 对整组一次 fsync,commit 逐个进入 InnoDB 提交。要能说出 leader 队列的语义和两个凑组参数的作用。把第 7 页的 redo 组提交接上:完整路径是两段组提交接力,双 1 下还有高 TPS 靠的就是这个。最后点一句组提交对从库并行回放的红利,为复制 deck 埋线。

Crash Recovery · §17.18.2

崩溃恢复:先前滚,再回滚,对账定生死

① 前滚(redo)

从最新 checkpoint LSN(存在 redo 文件头)开始扫描重放 redo,把数据页恢复到崩溃那一刻的物理状态——注意此时可能包含"已写页但事务未提交"的修改。

② 对账(XID)

恢复过程中发现 redo 处于 prepare 状态的事务:拿 XID 到 binlog 中查是否存在完整 XID event——在 → 补 commit;不在 → 标记回滚。binlog 裁决,主库向 binlog 对齐。

③ 回滚(undo)

对裁决为"未提交"的事务,沿 undo 的反向操作逐条撤销。回滚可能持续较长时间(大事务),但期间数据库已可用、锁按需释放——先快后准的两段式。

崩溃场景恢复动作
事务已 commit(redo commit 标记完整)redo 前滚已覆盖 → 保留,不碰
崩溃点 B:binlog 完整、redo 只有 prepareXID 对账命中 → 补提交(与从库一致)
崩溃点 A:redo prepare、binlog 没写XID 对账不中 → 回滚(主库从库都没有)
XA 事务停在 PREPARED(外部 XA)恢复后保持 prepared,等待 XA COMMIT / XA ROLLBACK 手动处置(手册 §17.18.2)
崩溃恢复三步曲:先 redo 从 checkpoint LSN 前滚到崩溃时刻,页面上可能有未提交的脏修改;然后 XID 对账裁决 prepare 事务的生死;最后 undo 回滚输家。四行表格把第 11 页两个崩溃点的下落对号入座。最后一个冷知识:外部 XA 的 prepared 事务恢复后仍停在 prepared,要人工 XA COMMIT 或 ROLLBACK,答出来是加分项。

Walkthrough · The Classic

串讲:一条 UPDATE 从进入 MySQL 到落盘

-- UPDATE t SET a=a+1 WHERE id=1; 的执行链

① 连接器   建立/复用连接 · 鉴权(8.0 已无查询缓存)
② 分析器   词法 + 语法解析
③ 优化器   选择索引(id 主键 vs 其他)
④ 执行器   调 InnoDB 接口,逐行处理

⑤ InnoDB:取行   id=1 所在页 → buffer pool
                  (不在则从磁盘读入)
⑥ InnoDB:写 undo  旧值 a=0 存入 update undo
⑦ InnoDB:改页    页内 a=0→1 · 写 redo buffer
                  该页变脏页
⑧ 提交    redo prepare + fsync(trx_commit=1)
          binlog write + fsync(sync_binlog=1)
          redo commit → 返回客户端 OK
⑨ 异步收尾 dump 线程推 binlog 给从库;
          page cleaner 择机刷脏页
追问答案落点
页不在内存怎么办?从磁盘读入 buffer pool(doublewrite 保护写回)
a=1 什么时候"永久"?内存页立刻可见(自己事务内);磁盘上要等脏页刷盘——但 redo 已落盘,崩溃也不丢
为什么先写 undo?崩溃后可能要回滚这条 UPDATE;回滚依据必须在修改前就位
从库什么时候有?binlog 落盘后 dump 线程推送 → 从库 relay log → 回放(见 复制 deck
综合题价值:这一题能同时考到执行器、buffer pool、undo、redo 2PC、binlog、复制——答的时候按"内存里发生了什么 / 日志里发生了什么 / 磁盘上发生了什么"三条线收拢,不慌不漏。
这条全流程是 MySQL 综合题的母题。前四步是 Server 层流水线,第五步开始进 InnoDB:读页、写 undo、改页写 redo,然后是标准的三段提交。右侧四个追问都是这条链上的挂钩点:页不在内存找 buffer-pool deck,历史版本找 mvcc.html,从库同步找 replication.html。答题技巧是按内存、日志、磁盘三条线收拢,显得系统而完整。

Cheat Sheet

一页带走:三份日志 · 刷盘三档 · 2PC · 崩溃恢复

① 三份日志:一句话分工

redo物理日志、InnoDB 层、循环写;崩溃后前滚重做已提交事务 → 保 D(持久性)
undo逻辑日志、InnoDB 层;回滚未提交事务 + MVCC 版本链 → 保 A(原子性)
binlog逻辑日志、Server 层追加写不覆盖;主从复制 + PITR(MyISAM 也有!)
不可替代性binlog 无页级信息、无 checkpoint,替不了 redo;redo 循环写会被覆盖、只有 InnoDB 有,替不了 binlog

② 持久化开关:双 1 与三档

innodb_flush_log_at_trx_commit1(默认)提交即 write+fsync,不丢|0 每秒写+刷,崩了丢约 1 秒|2 提交只 write 到 OS cache、每秒 fsync,mysqld 崩不丢、主机断电丢约 1 秒
sync_binlog1(默认)每组提交都 fsync;N = 攒 N 组才 fsync,最多丢 N−1 组
双 1两个开关都开到最严 —— 真正的 crash-safe 前提(代价:每事务两次 fsync)

③ redo 循环写与容量

两个指针write pos 往前写,checkpoint 往前推(之前的脏页已刷盘,空间可覆盖)
写满会怎样write pos 追上 checkpoint → 必须先刷脏推进 checkpoint → 期间更新全部阻塞,写入骤降
8.0.30 起innodb_redo_log_capacity(默认 100MB,可在线改)取代旧的 innodb_log_file_size / _in_group

④ 两阶段提交与三个崩溃点

为什么需要redo 与 binlog 分属两层、无法原子写;必须让主库恢复结果永远向 binlog 对齐
时序① redo prepare → ② 写 binlog → ③ redo commit
崩溃点 Aprepare 后、binlog 未写 → binlog 里查无此 XID回滚
崩溃点 Bbinlog 已写、commit 未写 → binlog 里有完整 XID event补提交
裁决依据拿 redo 中 prepare 事务的 XID 去 binlog 里对账

⑤ 崩溃恢复三步曲

① 前滚从最新 checkpoint LSN 起重放 redo,把库推回崩溃那一刻(含未提交修改
② 对账prepare 事务逐条用 XID 查 binlog,定生(补提交)死(回滚)
③ 回滚undo 把未提交事务逐个撤销
外部 XA恢复后仍停在 PREPARED,需人工 XA COMMIT/ROLLBACK

⑥ 吞吐与选型

组提交双 1 的两次 fsync 是瓶颈 → 同批事务合并一次 fsync;binlog 走 FLUSH → SYNC → COMMIT 三阶段,redo 侧同理
凑组参数binlog_group_commit_sync_delay(等多少微秒)/ _no_delay_count(凑够多少个就不等)
binlog 格式8.0 默认 ROW(确定性强)|STATEMENT 体积小、可能主从不一致|RC 只能用 ROWbinlog_format 8.0.34 起弃用
一句话背下来:三份日志都在解决"内存快但丢、磁盘稳但慢"——redo 向前重做、undo 向后撤销、binlog 给别人抄;2PC 用 XID 当裁判,主库恢复永远向 binlog 对齐。
速查页:六块按实战顺序排。先记三份日志的分工与"互相不可替代"(这是收束题的标准答案),再记双 1 与刷盘三档的语义差别(0/1/2 各自丢什么必须秒答),然后 redo 循环写与 8.0.30 参数更名,接着 2PC 三个崩溃点的下落(本 deck 最难的一块),再是崩溃恢复三步曲,最后是组提交与 binlog 格式选型。收尾句回扣第 3 页的最小心智模型。

Interview QA · 1/2 · 先自答再对照

基础 8 连问

先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的直接翻回第 16 页速查表。

1 · 为什么需要 redo log?

WAL随机写→顺序写

提交时刷随机数据页太慢。redo 让修改先以顺序追加方式落盘,脏页延迟刷;崩溃后重放 redo 恢复——用顺序小日志换随机大页写,同时保证持久性。

2 · redo log 和 binlog 的区别?

物理/逻辑循环/追加

四维:redo 在 InnoDB 层、binlog 在 Server 层;redo 物理日志、binlog 逻辑日志;redo 循环写、binlog 追加写;redo 管崩溃恢复、binlog 管复制与 PITR。

3 · innodb_flush_log_at_trx_commit 三档?

0 每秒1 提交 fsync2 写 OS cache

默认 1:提交即 write+fsync,不丢;0 每秒后台写+fsync,崩了丢约 1 秒;2 提交写到 OS page cache、每秒 fsync——mysqld 崩溃不丢、主机断电丢约 1 秒。

4 · 什么是"双 1"?各自防什么?

trx_commit=1sync_binlog=1

redo 与 binlog 的持久化开关都开到最严:前者保证崩溃后已提交事务能从 redo 恢复;后者保证已提交事务的 binlog 一定在磁盘上、从库一定能拿到。缺一个就有一份日志可能丢。

5 · undo log 为什么分 insert / update 两种?

回滚版本链

insert undo 只服务回滚、提交即丢;update undo 还要服务 MVCC 版本链(快照读找旧版本),必须等没有 ReadView 引用后由 purge 清理——丢弃时机完全不同。

6 · binlog 三格式怎么选?

ROW 默认RC 仅 row8.0.34 弃用

默认选 ROW:确定性强、复制安全,代价是体积。STATEMENT 体积小但不确定语句会导致主从不一致。MIXED 自动切换只是折中。binlog_format 设置自 8.0.34 起弃用,官方方向 row-only。

7 · sync_binlog 设 N 意味着什么?

攒 N 组最多丢 N−1

每 N 个事务组才 fsync 一次 binlog,fsync 成本降为 1/N;代价是崩溃时最近一批还没 fsync 的 binlog 丢失(连同对应已提交事务)——在从库上表现为"主库有、从库无"。

8 · redo 写满了会发生什么?

checkpoint 阻塞整体降速

write pos 追上 checkpoint,InnoDB 必须先推进 checkpoint——把对应脏页刷盘腾空间,期间所有更新阻塞,系统写入能力骤降。容量调大(innodb_redo_log_capacity)+ 合理 io_capacity 可缓解。

基础八连问覆盖 redo、undo、binlog 的主干。第二题的四维对比和第三题的三档语义必须秒答;第四题把双 1 说成"两份日志各自的持久化开关"而不是一句套话;第八题把 redo 写满和 buffer pool 的刷脏联系起来,能自然过渡到下一个 deck。

Interview QA · 2/2 · 先自答再对照

进阶 8 连问

9 · 为什么必须要两阶段提交?

两份日志写一半主从一致

redo 与 binlog 分属两层、无法原子写。先 commit redo 再写 binlog:崩溃后主库有、从库无;反序则从库有、主库回滚——都不一致。2PC 以 binlog 完整性(XID event)为裁决,让主库恢复结果永远向 binlog 对齐。

10 · 崩溃发生在 binlog 已写、redo commit 未写?

崩溃点 B补提交

恢复时该事务 redo 停在 prepare,拿 XID 查 binlog:存在完整 XID event → 补提交。与从库视角一致(binlog 里有),所以不破坏主从一致——这正是 2PC 设计的意义。

11 · 崩溃恢复的完整流程?

前滚对账回滚

① 从最新 checkpoint LSN 起 redo 前滚到崩溃时刻(可能含未提交修改);② prepare 事务用 XID 对账 binlog 定生死;③ undo 回滚未提交事务。外部 XA 的 prepared 事务保持原状等人工处置。

12 · 组提交解决什么问题?

fsync 摊薄FLUSH/SYNC/COMMIT

双 1 下每事务两次 fsync(redo + binlog)是吞吐瓶颈。binlog 组提交按 FLUSH→SYNC→COMMIT 三阶段把同批事务合并成一次 fsync;redo 侧同样有组提交。参数 binlog_group_commit_sync_delay 可主动凑大组。

13 · 有 binlog 了,还需要 redo 吗?反之呢?

互相不可替代

binlog 不能替代 redo:binlog 是逻辑日志、无页级信息、也没有 checkpoint 机制支撑"恢复到崩溃瞬间"。redo 不能替代 binlog:redo 循环写会被覆盖、且只有 InnoDB 有,复制与 PITR 需要全引擎、永不覆盖的逻辑日志。

14 · 什么是 crash-safe?

redo+undo+2PC重启后状态自洽

指"崩溃重启后,已提交事务不丢、未提交事务全部回滚",且主库状态与 binlog 一致。实现 = redo 前滚 + undo 回滚 + 两阶段提交对账。InnoDB crash-safe 的前提是双 1 等持久化配置正确。

15 · 长事务对三大日志各有什么影响?

undo 膨胀binlog 巨大

undo:长事务的 ReadView 钉住 update undo,purge 停滞、undo 表空间膨胀(详见 MVCC deck)。binlog:大事务一次性写入巨型 binlog event,主从传输与回放都卡顿。redo:未提交前其修改页迟迟不能 checkpoint 掉,拖累刷脏节奏。

16 · 8.0.30 之后 redo 容量怎么配?

innodb_redo_log_capacity默认 100MB

8.0.30 起用 innodb_redo_log_capacity(默认 100MB,可在线 SET GLOBAL),落在 #innodb_redo 目录约 32 个 #ib_redoN 文件;旧的 innodb_log_file_size / innodb_log_files_in_group 同步弃用——升级脚本要改参数名。

进阶组的主轴是两阶段提交的两个崩溃点推演和 XID 对账,第 13 题的"互相不可替代"是收束题,要求双向都说清。第 15 题把长事务和三种日志串起来,能展示跨 deck 的知识面。第 16 题是版本题,8.0.30 的参数更名是运维常踩的坑。

Related & References

相关知识点与参考

MySQL 领域 · 交叉链接

LRU 缓存(手撕实现) —— 哈希 + 双链表的 O(1) 结构与手撕
主从复制与高可用 —— binlog 如何流向从库、并行回放、半同步
事务基础 —— ACID 中原子性/持久性由谁保证(undo vs redo)
MVCC 多版本并发控制 —— update undo 版本链、purge 与长事务
InnoDB 锁体系 —— 2PC commit 阶段释放锁的时机衔接

答题串联 · 一图流

崩溃恢复 = redo 前滚 + XID 对账 + undo 回滚
提交 = redo prepare → binlog fsync → redo commit
双 1 = trx_commit=1 × sync_binlog=1
吞吐 = redo 组提交 × binlog 组提交(FLUSH/SYNC/COMMIT)
主从 = binlog → dump → relay log → 回放

参考来源(本 deck 全部版本敏感结论可溯源至下列一手材料)

dev.mysql.com/doc/refman/8.0/en/innodb-redo-log.html§17.6.5 Redo Log:WAL、循环写、checkpoint LSN、innodb_redo_log_capacity(8.0.30 起,默认 100MB)取代 ib_logfile 参数
dev.mysql.com/doc/refman/8.0/en/innodb-undo-logs.html§17.6.6 Undo Logs:insert/update undo 分类、purge、回滚角色
dev.mysql.com/doc/refman/8.0/en/replication-formats.html§19.2.1 Replication Formats:8.0 默认 ROW、MIXED 语义、binlog_format 自 8.0.34 弃用
dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.html§19.1.6.4:sync_binlog 默认 1、binlog_group_commit_sync_delay/_no_delay_count、组提交逻辑
dev.mysql.com/doc/refman/8.0/en/innodb-recovery.html§17.18.2 InnoDB Recovery:checkpoint 起点重放、undo 回滚未提交事务、XA PREPARED 处置
dev.mysql.com/worklog/task/?id=5223WL#5223 Group Commit of Binary Log:FLUSH / SYNC / COMMIT 三阶段定义
收尾页给出交叉链接和参考来源。这 deck 的所有版本敏感结论都有出处:redo 容量参数与目录结构出自 17.6.5,undo 分类出自 17.6.6,组提交三阶段出自 19.1.6.4 和 WL#5223,崩溃恢复出自 17.18.2,binlog 格式与弃用出自 19.2.1。复习时回到一手材料核对,不要背二手博客。