云计算百科
云计算领域专业知识百科平台

MySQL 崩溃以后怎么恢复数据?redo log、undo log 与 Crash Recovery

如果 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
↓
记录事务的数据变化

三个日志各有职责。


赞(0)
未经允许不得转载:网硕互联帮助中心 » MySQL 崩溃以后怎么恢复数据?redo log、undo log 与 Crash Recovery
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!