如果 MySQL 突然断电或者进程崩溃,Buffer Pool 中的数据全没了,InnoDB 到底是怎么恢复数据的?
例如服务器宕机前可能是:
事务 A:
已经 COMMIT
但是部分 Dirty Page 还没有刷盘
事务 B:
还没有 COMMIT
但是部分修改过的 Page
可能已经被刷到了磁盘
突然:
MySQL 崩溃
这时候就同时存在两个问题:
事务 A:
该保留的修改可能还没写入数据文件
事务 B:
不该保留的修改却可能已经写入数据文件
InnoDB 怎么处理?
答案就是:
先利用 redo log 恢复应该存在的数据页修改,再利用 undo log 回滚没有提交完成的事务。
这就是 Crash Recovery 的核心思想。
1. 什么是 Crash Recovery?
Crash Recovery 就是:
MySQL 在发生异常宕机之后,重新启动时自动把 InnoDB 恢复到一致状态的过程。
例如:
- 操作系统突然崩溃
- 服务器断电
- mysqld 进程被强制终止
- 机器突然重启
都可能导致:
Buffer Pool
中的内容直接丢失
但是磁盘上的:
redo log
undo log
数据文件
等仍然存在。
InnoDB 就利用这些持久化信息完成恢复。
MySQL 官方也把事务、COMMIT、ROLLBACK 和 crash recovery 作为 InnoDB ACID 能力的重要组成部分。
2. 为什么崩溃以后不能直接使用磁盘数据?
因为:
事务提交成功,并不意味着所有 Dirty Page 都已经写回磁盘。
例如执行:
UPDATE account
SET balance = 900
WHERE id = 1;
事务已经:
COMMIT
但可能仍然是:
Buffer Pool:
balance = 900
磁盘数据文件:
balance = 1000
因为内存中的 Page 是 Dirty Page。
只要对应 redo log 已经满足持久化要求,事务就不需要等待整个数据页刷盘。
如果此时突然宕机:
Buffer Pool
↓
全部丢失
磁盘里却还是:
balance = 1000
所以如果 MySQL 重启以后直接相信数据文件:
已经提交的 900 就丢了。
这就是为什么需要 redo log。
3. redo log 在崩溃恢复中做什么?
redo log 记录的是数据页发生修改时所需要的重做信息。
它最大的作用之一就是:
恢复那些已经发生、但还没有完整写入数据文件的 Page 修改。
例如:
磁盘:
balance = 1000
redo:
这个 Page 后来发生过修改
1000 → 900
MySQL 重启以后:
读取 redo
↓
重新应用需要恢复的修改
↓
balance = 900
这就是 redo 这个名字的含义:
redo = 再做一次
MySQL 8.4 官方明确说明,redo log 是用于 crash recovery 的磁盘结构;异常关闭之前没有完成写入数据文件的修改,会在初始化时自动重新应用。
4. 那 undo log 又是干什么的?
现在再看另一种情况。
假设事务 B:
START TRANSACTION;
UPDATE account
SET balance = 500
WHERE id = 2;
但是还没有:
COMMIT;
服务器就崩了。
你可能会想:
没有 COMMIT,修改应该还只存在 Buffer Pool,宕机以后内存丢了,不就自动恢复了吗?
不一定。
因为 InnoDB 允许脏页在事务提交之前就刷入磁盘。
例如:
事务 B:
balance = 1000
↓
修改
↓
balance = 500
但还没有 COMMIT
此时某个 Page 可能已经被后台刷盘:
磁盘:
balance = 500
然后突然:
💥 宕机
那么磁盘上已经出现了:
一个未提交事务的修改。
这时候就需要 undo log。
5. 所以 redo 和 undo 分别解决什么问题?
redo:
把应该存在的修改重新做出来。
undo:
把不应该存在的修改撤销掉。
例如:
事务 A:
已经 COMMIT
但是 Page 没刷盘
↓
redo 恢复
而:
事务 B:
没有 COMMIT
但是 Page 已经刷盘
↓
undo 回滚
所以整个恢复过程可以简单理解为:
Crash
↓
redo
↓
把数据页恢复到崩溃时应该具有的状态
↓
undo
↓
回滚未完成事务
↓
得到一致的数据状态
这就是 redo + undo 的核心配合。
6. 例子
假设初始数据:
A = 100
B = 100
现在有两个事务。
事务 T1:
UPDATE account
SET balance = 200
WHERE id = 1;
COMMIT;
事务 T1 已经提交。
但是:
对应 Dirty Page
还没有刷盘
所以磁盘可能仍然:
A = 100
与此同时事务 T2:
START TRANSACTION;
UPDATE account
SET balance = 300
WHERE id = 2;
但是 T2:
没有 COMMIT
恰好 T2 修改的数据页已经被后台刷盘。
于是宕机前:
T1:
应该是 A = 200
但磁盘还是 A = 100
T2:
不应该保留 B = 300
但磁盘已经是 B = 300
突然:
💥 Crash
磁盘现在可能是:
A = 100
B = 300
但正确结果应该是:
A = 200
B = 100
怎么办?
7. 第一步:先应用 redo
MySQL 重启以后,InnoDB 会进行 redo log application。
根据 redo,把需要恢复的 Page 修改重新应用。
于是:
磁盘:
A = 100
B = 300
↓ redo
恢复相关 Page 修改
这里非常重要:
redo 并不是简单只恢复“已经提交事务”的修改。
redo 首先关注的是:
数据页曾经发生过哪些需要恢复的修改。
因此恢复后的状态可以先理解成接近:
A = 200
B = 300
也就是说:
事务 T1 的修改恢复出来了。
事务 T2 的页面修改也可能存在。
这没问题。
因为:
下一步还有 undo。
官方的恢复流程也是先进行 redo log application,然后再处理未完成事务的回滚。
8. 第二步:回滚未完成事务
redo 应用以后,InnoDB 还需要检查:
崩溃发生时,有哪些事务没有正常完成?
例如:
T1:
COMMIT
应该保留。
而:
T2:
ACTIVE
没有 COMMIT
就不能保留。
所以 InnoDB 根据 undo log:
B = 300
↓
undo
↓
B = 100
最终:
A = 200
B = 100
数据库重新恢复到一致状态。
MySQL 官方明确说明,InnoDB 会自动回滚崩溃时存在的未提交事务。
9. 为什么不直接只 redo 已提交事务?
你可能会想:
既然最终只需要已提交事务,为什么 redo 阶段不直接判断这个 redo 属于已提交还是未提交?
因为 redo 的设计重点是:
Page 级别的物理恢复。
redo 更关心的是:
某个 Page
发生过哪些修改
而事务最终:
提交
还是
回滚
属于更高层次的事务逻辑。
所以 InnoDB 把两件事分开:
redo
↓
恢复 Page 修改
undo
↓
处理事务回滚
这样职责非常清晰。
可以记成:
redo:
先恢复“物理现场”
undo:
再清理“不应该留下的事务修改”
10. 为什么 redo 要先执行?
假设未提交事务 T2 修改了某个 Page。
这个 Page:
根本还没有完整写到磁盘
但是 undo 自己需要的数据结构也属于 InnoDB 页面体系的一部分。
如果 Page 状态都还没有先恢复好:
InnoDB 可能连完整的事务恢复基础都没有。
所以从整体理解上:
先通过 redo
恢复必要的数据页状态
↓
再根据事务信息和 undo
回滚未完成事务
更加合理。
这也就是为什么 Crash Recovery 不是简单的:
找到没提交事务
↓
直接 undo
而是需要先进行 redo recovery。
11. redo 从哪里开始恢复?
假设 MySQL 已经运行了:
30 天
redo log 中经历了海量修改。
崩溃以后难道要:
从数据库创建的第一天开始把所有 redo 重放一遍?
显然不可能。
所以 InnoDB 需要一个非常重要的机制:
Checkpoint
12. 什么是 Checkpoint?
Checkpoint 表示:在这个位置之前需要保证的旧修改,已经有足够的数据页状态持久化到磁盘,因此 Crash Recovery 不需要再从更早的位置重新做起。
InnoDB 会记录:
Checkpoint LSN
服务器崩溃以后:
找到最近 Checkpoint
↓
从 Checkpoint 附近开始扫描 redo
↓
应用之后需要恢复的修改
而不是:
从 redo 历史最开始的位置
一路恢复到最后
MySQL 官方明确说明,在 crash recovery 时,InnoDB 会寻找日志中的 checkpoint 标记,并从该 checkpoint 向前扫描 redo、应用之后的修改。
13. 什么是 LSN?
Log Sequence Number
可以简单理解成:
redo log 中不断向前增长的位置编号。
随着 redo 不断产生:
LSN 100
LSN 200
LSN 300
LSN 400
LSN 500
不断向前增长。
例如:
Checkpoint LSN = 300
Current LSN = 500
那么 crash recovery 大致关注:
300
↓
400
↓
500
这个范围中的 redo。
MySQL 8.4 还提供了诸如 Innodb_redo_log_checkpoint_lsn、Innodb_redo_log_current_lsn、Innodb_redo_log_flushed_to_disk_lsn 等状态变量,用来表示 checkpoint、当前 redo 位置和已持久化位置。
14. Checkpoint 是把整个 Buffer Pool 一次性刷盘吗?
不是。
你可能会认为:
Checkpoint
↓
暂停数据库
↓
把所有 Dirty Page 全刷盘
↓
继续运行
InnoDB 并不是这么做的。
它采用的是:
Fuzzy Checkpoint
可以理解成:
后台逐渐把 Dirty Page 分批刷盘,然后不断向前推进 Checkpoint。
官方明确说明,InnoDB 使用 fuzzy checkpoint,不需要一次性把整个 Buffer Pool 全部刷盘,而是以小批量方式持续 Flush 修改过的数据页。
所以更接近:
Dirty Page
Dirty Page
Dirty Page
Dirty Page
↓
后台逐步 Flush
↓
Checkpoint LSN
逐渐向前推进
而不是:
停止整个数据库
↓
一次性刷完全部脏页
15. 为什么 Checkpoint 很重要?
主要有两个原因。
第一:
减少 Crash Recovery 需要扫描和重做的 redo 范围。
例如没有 Checkpoint:
redo:
1 → 1000000
崩溃以后可能需要处理大量日志。
有 Checkpoint:
Checkpoint = 950000
Current = 1000000
就只需要重点处理:
950000 → 1000000
恢复速度明显更合理。
第二:
允许旧 redo 空间被重新利用。
redo log 的空间不是无限增长的。
当某些旧 redo 已经对应到:
相关 Dirty Page
已经安全刷盘
以后,这些旧日志就不再是 Crash Recovery 必需的信息。
因此对应日志空间可以被后续 redo 重用。
MySQL 的 redo 实现文档也说明,last_checkpoint_lsn 之前的 redo 已经不再需要用于恢复,可以被覆盖重用。
16. WAL、Checkpoint、Crash Recovery 是什么关系?
首先 WAL 保证:
对应 Dirty Page 刷入数据文件之前,相关 redo 必须先持久化。
于是:
修改 Buffer Pool
↓
产生 Dirty Page
+
产生 redo
↓
redo 先持久化
↓
Dirty Page 后续才能安全刷盘
然后后台:
不断 Flush Dirty Page
↓
Checkpoint 向前推进
如果发生:
💥 Crash
则:
找到最近 Checkpoint
↓
扫描之后的 redo
↓
恢复需要恢复的数据页修改
所以:
WAL
↓
确保恢复信息先存在
Checkpoint
↓
确定恢复从哪里开始
redo
↓
真正重做 Page 修改
三者并不是三个孤立的知识点。
17. 一张图把正常运行和崩溃恢复连起来
正常运行:
UPDATE
↓
修改 Buffer Pool
↓
Dirty Page
+
redo
↓
redo 持久化
↓
COMMIT
↓
后台 Flush Dirty Page
↓
Checkpoint 不断推进
突然:
Crash
重新启动:
读取 Checkpoint LSN
↓
扫描后续 redo
↓
Redo Log Application
↓
恢复 Page
↓
识别未完成事务
↓
使用 undo 回滚
↓
数据库恢复一致
这就是 InnoDB Crash Recovery 的主线。
18. 一个已经 COMMIT 的事务一定能恢复吗?
这里必须加一个很重要的前提:
取决于 redo 是否真正达到了对应的持久化条件。
默认可靠配置下,事务提交时会保证相应日志持久化。
但 MySQL 存在参数:
innodb_flush_log_at_trx_commit
它会影响事务提交时 redo 的写入和刷盘策略。
如果为了性能使用了更弱的持久化配置,那么:
服务器突然掉电时,最近一小段已经返回 COMMIT 成功的事务仍可能丢失。
MySQL 8.4 官方文档也明确指出,弱化日志刷盘策略可能在崩溃时损失最近的事务;对于需要完整 ACID durability 的场景,应使用可靠的日志同步配置。
所以不能把 Crash Recovery 理解成:
无论怎么配置,只要客户端看到 COMMIT,就绝对永远不会丢。
正确理解是:
Crash Recovery 能恢复已经可靠持久化到 redo 中的信息;持久性本身还受到日志刷盘配置影响。
19. 两阶段提交在 Crash Recovery 时又怎么发挥作用?
前面讲 UPDATE 时,我们学过:
InnoDB PREPARE
↓
写 binlog
↓
InnoDB COMMIT
现在考虑最危险的情况:
InnoDB PREPARE
↓
binlog 已经成功
↓
💥 Crash
↓
InnoDB 还没完成最终 COMMIT
重启以后怎么办?
不能简单看到:
PREPARED
就回滚。
因为 binlog 已经成功写入了这个事务。
MySQL 重启恢复时会检查 binary log 中完整记录的事务 XID,并告诉存储引擎:
prepared transaction
如果 XID 在完整 binlog 中
↓
提交
如果不在
↓
回滚
MySQL 8.4 官方文档明确说明,服务器重启时会扫描最新 binary log 中的事务标识,并让 InnoDB 完成已经成功写入 binlog 的 prepared transaction,从而保证 binlog 和 InnoDB 数据保持一致。
这样上一章的:
两阶段提交
就和这一篇的:
Crash Recovery
真正连起来了。
20. 所以 binlog 会参与 InnoDB 的页面恢复吗?
不会直接替代 redo。
需要区分两个职责。
redo log:
恢复 InnoDB 数据页。
binlog:
在事务协调场景下,帮助确定某些 PREPARED 事务最终应该 COMMIT 还是 ROLLBACK。
所以:
redo
↓
Page Recovery
binlog
↓
Transaction Commit Coordination
这也是为什么前面我们说:
不能只留下 binlog,然后不要 redo log。
binlog 根本不是 InnoDB 的 Page 级恢复日志。
21. 那 undo log 会不会把已提交事务回滚掉?
不会。
undo log 本身只是:
提供“如何撤销修改”的信息。
最终到底要不要执行 undo,要看事务状态。
例如:
事务 A:
COMMITTED
那么对应修改应该保留。
不会因为存在 undo 就把它撤销。
而:
事务 B:
Crash 时仍然 ACTIVE
则需要:
undo
↓
ROLLBACK
所以:
undo 是撤销能力,不等于所有修改最终都会被撤销。
这和正常执行:
ROLLBACK;
是同一个道理。
22. Crash Recovery 完成前 MySQL 一直不能连接吗?
这里实际实现比我们画的简单流程更复杂。
MySQL 官方恢复流程中,redo log application 会在初始化阶段执行,并且是在接受连接之前完成。
但 redo application 之后,InnoDB 会尽可能早地开始接受连接。
例如:
未完成事务的 rollback
可以由后台线程继续执行。
也就是说,实际情况可能是:
redo recovery 完成
↓
开始允许新的连接
↓
后台继续 rollback 某些旧事务
↓
后台继续 purge 等工作
因此如果崩溃前存在一个非常大的未提交事务:
MySQL 虽然可能已经可以提供服务,但后台 rollback 仍然可能持续很长时间。
官方文档也指出,Crash Recovery 中对未完成事务的 rollback 可以在接受新连接后由后台线程继续执行,并且恢复中的事务在 rollback 完成前仍可能导致锁冲突。
23. 一个超大事务为什么会让恢复很慢?
假设执行:
START TRANSACTION;
UPDATE huge_table
SET status = 1;
一次修改了几千万行。
执行很久以后:
还没有 COMMIT
突然服务器崩溃。
重启以后:
redo
↓
恢复必要页面
接下来发现:
这个超大事务没有提交
那么就必须:
undo
↓
逐步回滚大量修改
显然会很慢。
MySQL 官方甚至说明,大型未完成事务的 rollback 时间可能明显长于事务之前运行的时间。
这也是为什么我们一直强调:
尽量避免超长、超大的事务。
它不仅会:
- 长时间持有锁
- 保留大量 undo 历史
- 影响 MVCC
- 增加日志压力
还可能:
让崩溃后的恢复和 rollback 变得非常慢。
24. redo 能解决所有磁盘写入损坏问题吗?
不能。
这里再补一个非常重要的概念:
Doublewrite Buffer
假设一个 Page 默认是:
16KB
现在 InnoDB 正准备把整个 Page 写入数据文件:
16KB Page
结果只写了一半:
写了 8KB
↓
突然断电
那么这个 Page 可能变成:
Torn Page
也就是:
页面本身只写了一部分,结构已经损坏。
这和普通的:
整个 Page 还是旧版本
不是一回事。
InnoDB 为这种问题提供了 Doublewrite Buffer。
25. Doublewrite Buffer 是怎么工作的?
在 Page 真正写到数据文件对应位置之前,InnoDB 会先把 Page 写到:
Doublewrite Buffer
大致:
Dirty Page
↓
先写 Doublewrite Buffer
↓
确认完成
↓
再写真正 data file
如果第二次写真正数据文件时:
写了一半
↓
💥 Crash
恢复时可以从 Doublewrite Buffer 找到一个完整 Page 副本进行修复。
MySQL 8.4 官方文档明确说明,doublewrite buffer 就是用来在 Page 写入中途发生系统、存储或 mysqld 崩溃时,恢复这种 incomplete page write 的。
所以:
redo log 和 Doublewrite Buffer 也不是一回事。
26. redo 和 Doublewrite 分别解决什么?
可以这样理解。
普通情况:
Page 完整
但是版本比较旧
↓
redo
把缺失修改重做
而 Torn Page:
一个 16KB Page
只写进去一部分
↓
Page 本身已经损坏
↓
Doublewrite Buffer
提供完整 Page 副本
因此:
Doublewrite
↓
先解决 Page 是否完整
redo
↓
再解决 Page 是否包含最新需要恢复的修改
这两个机制配合,Crash Recovery 才更加可靠。
27. Crash Recovery 的完整流程怎么理解?
可以概括成:
MySQL Crash
↓
重新启动
↓
找到 Tablespace
↓
找到 Checkpoint LSN
↓
扫描 redo log
↓
Redo Log Application
↓
恢复必要的数据页修改
↓
处理 PREPARED 事务状态
↓
binlog / transaction coordinator
帮助判断最终提交或回滚
↓
回滚未完成事务
↓
undo
↓
后台继续 purge 等工作
↓
数据库恢复正常
真实 MySQL 内部还有更多步骤,例如 tablespace discovery、change buffer merge、purge 等,但我们现在最应该掌握的是:
Checkpoint
↓
Redo
↓
Transaction State
↓
Undo
这条主线。MySQL 官方列出的 Crash Recovery 过程也包括 tablespace discovery、redo log application、rollback incomplete transactions、change buffer merge 和 purge 等步骤。
28. 正常关闭为什么通常不需要这么重的恢复?
如果 MySQL 正常关闭:
正常 Shutdown
InnoDB 有机会完成必要的日志和后台处理。
因此下一次启动通常不需要像异常 Crash 那样处理大量 redo recovery。
InnoDB redo 实现文档也指出,在正常 shutdown 情况下,redo 在逻辑上通常已经不再存在需要从 checkpoint 之后重新应用的记录;而某些 fast shutdown 模式会把更多恢复工作留到下次启动。
所以:
正常关闭
和:
直接 kill / 断电
不是同一种启动恢复成本。
29. Crash Recovery 和备份恢复是一回事吗?
不是。
这个也非常重要。
Crash Recovery:
数据库自己发生异常宕机以后,利用 redo、undo 等机制恢复当前数据库。
例如:
服务器突然断电
↓
重启 MySQL
↓
InnoDB 自动 Crash Recovery
而:
Backup Recovery
或者:
Point-In-Time Recovery
解决的是:
数据文件真的丢了、磁盘损坏了,或者用户误删数据以后怎么办?
例如:
DROP TABLE important_table;
这是一个正常执行并提交成功的操作。
Crash Recovery 不会说:
“这是误操作,我帮你撤销。”
因为从数据库角度:
事务正常执行
正常提交
它没有崩溃一致性问题。
这种情况需要:
Backup
+
binlog
↓
PITR
恢复。
所以:
Crash Recovery
≠
Backup Recovery
这是两套不同的问题。
30. redo log、undo log、binlog 怎么区分?
redo log 属于 InnoDB。
核心作用:
Crash Recovery
主要解决:
应该存在的 Page 修改还没完整写入数据文件怎么办?
undo log 属于 InnoDB。
核心作用:
ROLLBACK + MVCC
在 Crash Recovery 中主要解决:
未提交事务的修改已经进入数据页怎么办?
binlog 属于 MySQL Server。
核心作用:
Replication + PITR
同时在 InnoDB 两阶段提交恢复中,还可以帮助:
判断某些 PREPARED 事务最终应该提交还是回滚。
所以:
redo
↓
重做
undo
↓
撤销
binlog
↓
记录事务的数据变化
三个日志各有职责。
网硕互联帮助中心






评论前必须登录!
注册