大家好,我是数据库小学妹 👋
生产环境最怕一个问题:数据库突然宕机,已提交的数据会不会丢?早期我也不确定。直到我把InnoDB的崩溃恢复机制从头到尾理了一遍,才真正放下心来。
今天我把这个机制拆开讲。从WAL到checkpoint,从前滚到回滚,用实验模拟一次崩溃,看看InnoDB怎么把数据救回来。如果你对数据库的可靠性有疑虑,这篇能给你答案。
崩溃恢复的前提:WAL机制
WAL全称Write-Ahead Logging。预写式日志。核心思想就一句话:数据页落盘前,redo日志必须先写。
为什么?顺序写快,随机写慢。先写redo,再慢慢刷页。写到一半崩了也不怕,日志还在就能重做。
MySQL宕机数据怎么恢复?答案就在这个机制里。
InnoDB的redo log是一组循环使用的文件。默认两个,每个1GB:
ib_logfile0 ← 写满后切换到
ib_logfile1 ← 写满后回到 ib_logfile0(循环覆盖)
redo日志记录了什么?每个物理数据页的修改。不是SQL语句,不是逻辑操作,是底层的"把第X页第Y行的Z字段从旧值改成新值"这种级别的记录。
-- 执行一条UPDATE
UPDATE orders SET amount = 500 WHERE order_id = 1;
-- redo log中记录的是类似这样的内容:
-- [LSN:12345678] Page:3, Offset:256, Change: amount字段 200→500
LSN是Log Sequence Number,日志序列号。每次修改递增。崩溃恢复时按LSN顺序重放。我搞错过一个概念,以为redo log记的是SQL语句。其实它记的是物理页的字节级变化。这就是为什么redo log文件二进制不可读。它不是文本日志,是数据页的"修改痕迹"。
redo log的刷盘策略由innodb_flush_log_at_trx_commit控制。三个值,三种安全等级:
| 值 | 行为 | 性能 | 安全性 |
|---|---|---|---|
| 0 | 每秒刷一次 | 最快 | 崩溃可能丢1秒数据 |
| 1 | 每次提交刷盘 | 最慢 | 不丢数据(推荐) |
| 2 | 每次提交写OS缓存 | 中等 | 可能丢操作系统级崩溃 |
生产环境必须设成1。这是默认值,但有人为了性能改成0。一旦崩溃,丢失的就是那些"提交了但没刷盘"的事务。
checkpoint:InnoDB的"存档点"
如果每次崩溃都要从第一条redo log开始重放,恢复时间会很长。
所以InnoDB引入了checkpoint机制。简单说,它就是一个"存档点"。
checkpoint时刻之前,所有已提交事务的数据页已经刷到磁盘。checkpoint之后的修改,还在redo log里等着重放。下次恢复时,只需从checkpoint开始重放,不用从头来。
checkpoint和Buffer Pool直接相关。InnoDB的脏页都在Buffer Pool里。fuzzy checkpoint刷脏时,不是全量刷,而是分批刷。只有当某个LSN之前的所有脏页都落盘后,checkpoint才会推进到那个位置。这个推进速度决定了恢复时需要重放的redo log量。checkpoint分两种:
Sharp checkpoint。 正常关闭数据库时触发。InnoDB把所有脏页刷盘,然后记一个checkpoint。下次启动时直接跳到这个checkpoint,不需要做恢复。因为所有数据都已经在磁盘上了。
Fuzzy checkpoint。 运行期间异步触发。不会暂停数据库,而是在后台慢慢刷脏页。刷完一批,checkpoint位置就往前推进一段。
-- 查看checkpoint相关状态
SHOW ENGINE INNODB STATUS\G
-- 输出中的关键信息:
-- Log sequence number 12345678 ← 当前LSN
-- Log flushed up to 12345670 ← 已刷盘的LSN
-- Pages flushed up to 12345000 ← checkpoint对应的LSN
-- Last checkpoint at 12345000 ← 最近的checkpoint位置
-- 恢复进度怎么看?盯error log。
-- MySQL启动时会输出:
-- "Doing recovery: scanned up to log sequence number XXX"
-- 看到这个数字在不断推进,说明恢复在进行中。
12345678减去12345000,等于678。这意味着有678个单位的redo log需要在恢复时重放。checkpoint越近,恢复越快。所以InnoDB会尽可能频繁地做fuzzy checkpoint。
崩溃恢复的两个阶段:前滚 + 回滚
数据库宕机后重启,InnoDB自动进入崩溃恢复流程。这个过程不需要人工干预。第一阶段:前滚(Redo)。 从最近的checkpoint开始,把redo log里的操作重新做一遍。已提交但还没刷盘的修改,重新应用到数据页。
崩溃时刻
↓
[checkpoint] ──────── [已提交事务A] ── [已提交事务B] ── [未提交事务C]
✓ 重做 ✓ 重做 ✗ 跳过
前滚的结果是:把所有已提交事务的修改恢复到数据页。未提交的事务也会被前滚(因为redo log不分提交状态),但它们会在第二阶段被回滚。
第二阶段:回滚(Undo)。 前滚完成后,InnoDB扫描undo log,找出前滚阶段被恢复但未提交的事务,把它们回滚。
前滚完成后:
事务A:已提交 ✓ → 保留
事务B:已提交 ✓ → 保留
事务C:未提交 ✗ → 回滚(用undo log撤销)
整个过程自动化。你只需要等。大事务回滚可能很慢,因为undo是单线程的。
-- 查看回滚进度(MySQL 5.7+)
SELECT r.trx_id AS rolling_back_trx_id,
r.trx_started,
r.trx_query AS rolling_back_query,
TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) AS running_seconds
FROM information_schema.INNODB_TRX r;
实验:模拟一次崩溃恢复
光说不练不踏实。我来模拟一次真实的崩溃恢复。
准备环境。 创建一个测试表,插入数据,然后强制杀进程模拟崩溃。
-- 创建测试表
CREATE TABLE crash_test (
id INT PRIMARY KEY AUTO_INCREMENT,
data VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- 插入测试数据
INSERT INTO crash_test (data) VALUES ('崩溃前的数据');
INSERT INTO crash_test (data) VALUES ('这条也应该在');
COMMIT;
-- 再插入一条但不提交
START TRANSACTION;
INSERT INTO crash_test (data) VALUES ('这条不该在');
-- 不COMMIT,直接杀进程
# 强制杀掉MySQL进程,模拟崩溃
kill -9 $(cat /var/run/mysqld/mysqld.pid)
# 重启MySQL
systemctl start mysqld
验证结果。 重启后查数据:
SELECT * FROM crash_test;
+----+----------------+---------------------+
| id | data | created_at |
+----+----------------+---------------------+
| 1 | 崩溃前的数据 | 2026-07-10 10:00:00 |
| 2 | 这条也应该在 | 2026-07-10 10:00:01 |
+----+----------------+---------------------+
前两条数据回来了。第三条(未提交的那条)不在了。这就是崩溃恢复的效果:已提交的不丢,未提交的不留。
看恢复日志。 启动时的error log记录了恢复过程:
[Note] InnoDB: Database was not shutdown normally! ← 检测到非正常关闭
[Note] InnoDB: Starting crash recovery. ← 开始崩溃恢复
[Note] InnoDB: Reading tablespace information... ← 读取表空间信息
[Note] InnoDB: Doing recovery: scanned up to LSN 12345678 ← 前滚扫描redo log
[Note] InnoDB: Starting an apply batch of log records... ← 开始批量重放
[Note] InnoDB: Apply batch completed ← 前滚完成
[Note] InnoDB: Scanning undo logs... ← 扫描undo log
[Note] InnoDB: Rollback of non-committed transactions completed ← 回滚完成
[Note] InnoDB: Crash recovery completed. ← 恢复完成
如果数据文件坏了怎么办?
redo log只能恢复逻辑上已提交但没刷盘的数据。它不能修复物理损坏。
如果磁盘故障导致.ibd文件损坏,redo log也无能为力。这时候需要备份恢复。PITR(Point-in-Time Recovery)用binlog做基于时间点的恢复,那是另一套机制。
崩溃恢复要多久?取决于checkpoint位置。checkpoint越近,重放的日志越少,恢复越快。
我搞错过一次概念。以为WAL能解决所有数据丢失问题。其实WAL只管崩溃恢复,不管硬件故障。两者是不同的场景。
崩溃恢复是所有关系型数据库的标配。底层原理都一样:WAL + checkpoint + 前滚回滚。只是名字不同。MySQL叫redo log。Oracle叫redo log加archive log。SQL Server叫transaction log。国产数据库也遵循这套机制,只是实现细节各有差异。
比如金仓数据库的崩溃恢复,同样基于WAL日志。恢复过程也是前滚加回滚。不同的是,金仓支持指定恢复目标时间点,通过配置文件设置恢复目标和WAL路径,就能精确恢复到故障前的某个时刻。这在做误操作回滚时特别有用。
但不管名字怎么变,核心逻辑不变。日志先行,检查点推进,崩溃后重做已提交、撤销未提交。
崩溃恢复避坑清单
innodb_flush_log_at_trx_commit别改成0。 有人为了性能调成0,说吞吐量提升30%。没错,吞吐量是提升了。但崩溃时丢的是最后一秒内所有已提交事务。你确定你的业务能承受?我见过电商系统因为这个问题丢了上百笔订单,事后对账对到凌晨。
redo log文件大小和数量要合理配置。 默认两个1GB文件。如果业务写入量大,redo log很快写满触发checkpoint,性能会抖动。适当增大文件(比如2GB×3个),减少checkpoint频率。但别太大,恢复时间会变长。
大事务回滚很慢,要有耐心。 一个跑了两个小时的事务,崩溃后回滚可能也要两个小时。undo是单线程执行的。这时候千万别再杀进程,否则回滚中断,下次启动又要重新回滚,无限循环。正确的做法是等它回滚完,监控information_schema.INNODB_TRX看进度。
恢复总结
崩溃恢复是数据库可靠性的最后一道防线。InnoDB用WAL + checkpoint + 两阶段恢复,保证已提交的事务不丢,未提交的事务不留。日常运维能做的,是确保redo log配置合理,flush策略设成1,定期做备份演练。别等真崩了才发现恢复不了。
对数据库来说,崩溃不是最可怕的。最可怕的是你不知道崩溃后能恢复到什么程度。
朋友,你的数据库做过崩溃恢复演练吗?redo log配置是多少?欢迎在评论区聊聊。
我是数据库小学妹,咱们下篇见 👋