InnoDB 表损坏修复实战:innodb_force_recovery 逐级抢救、mysqldump 分片导出与 ibd 硬恢复全流程

数据库"打不开"的那一刻,先别慌

凌晨两点,网站报 500。你去翻 MySQL 日志,看到一行红字:[ERROR] InnoDB: Database page corruption on disk or a failed file read of page [page id: space=...]。再一连接数据库,表读不了;严重的时候,MySQL 直接启动不了,报 InnoDB: Table ... is corrupted。这一刻很多站长第一反应是重装、是恢复备份,但其实有很大一部分损坏是可以原地救回来、甚至无损恢复的。关键在于你慌不慌、动手顺序对不对。

这篇文章讲清楚 InnoDB 表损坏到底是怎么发生的、怎么判断损坏范围和类型、innodb_force_recovery 六个级别分别该什么时候用、以及最坏情况下怎么从 .ibd 文件里把数据硬抢救出来。全文基于真实排障顺序,直接可以当手册用。

为什么会损坏:先搞清楚敌人

InnoDB 的持久性建立在"写日志 + 落盘"配合之上,损坏通常来自这几类原因:

  • 非正常掉电或强制 kill -9。 数据页正在写一半,或者 redo log 与数据文件不一致,重启后校验失败。这是最常见的。
  • 服务器 OOM 时被内核杀进程。 尤其是 redo log 正写到一半。
  • 磁盘/文件系统故障。 坏道、SSD 掉电写坏、ext4 的 write barrier 被关掉、虚拟机被宿主机强停。
  • 硬件内存故障。 内存条坏了导致写下去的数据本身就是坏的,这类最隐蔽。
  • 人为误操作。 比如直接把 ibdata1 拷来拷去、用不同版本的 mysqld 去开老数据目录。

注意一个重要前提:MySQL 8.0 之后,默认 innodb_file_per_table=ON,每张表一个 .ibd 文件。这大大降低了"一张表坏了全库都开不了"的概率,也让单表抢救变得可行。如果你的库还在老版本、所有表挤在 ibdata1 里,拯救难度会指数级上升,趁早升级或至少确认 file_per_table 是开的。

第一步:先判断损坏在"日志层"还是"数据层"

动手之前先做一件最重要的事:马上把整个数据目录冷备一份,或者至少把报错的那张表的 .ibd 和 ibdata1、#innodb_redo/ 拷走。理由是:修复过程本身可能把状态越改越乱,没有原始副本就没法重来。命令示例:

systemctl stop mysql
cp -a /var/lib/mysql /var/lib/mysql.bak.$(date +%s)
# 或者用 rsync 保留所有属性
rsync -aHAX --numeric-ids /var/lib/mysql/ /backup/mysql-cold/

拷完再开始折腾。这一步千万别跳。

然后看错误日志,判断损坏范围:

tail -100 /var/log/mysql/error.log | grep -iE 'corrupt|error|innodb'

如果日志里只提到某一张表(space id 对应单表),那是单表损坏,好办;如果提到 ibdata1、Double write、redo,涉及系统表空间或日志,就麻烦得多。先把范围界定清楚,再决定策略。

第二步:能启动的话,先用 CHECK TABLE 和 innodb_force_recovery 起步

如果 MySQL 还能启动(哪怕是只允许写入 0),先登录进去对可疑表做检查:

CHECK TABLE your_db.your_table;
-- 或者更严格的扩展检查(会慢,但信息更多)
CHECK TABLE your_db.your_table EXTENDED;

返回 status: OK 说明这张表没问题;返回 Corrupt 或 error 就锁定了目标。

如果 MySQL 根本起不来,就要靠 innodb_force_recovery 这个"抢救模式"开关。它从 1 到 6 逐步增强,宗旨是"能读出来多少算多少",越大越能启动,但越不安全。配置写在 my.cnf 的 [mysqld] 段:

[mysqld]
innodb_force_recovery = 1

六个级别的含义,这是核心,务必对号入座:

  • 1(SRV_FORCE_IGNORE_CORRUPT):遇到损坏的页直接跳过,让服务器能起来。只读,不写。绝大多数"只坏了一两个页"的情况,1 就能启动。
  • 2(SRV_FORCE_NO_BACKGROUND):阻止后台清理线程(purge)运行。适合崩溃发生在 purge 过程中、或 purge 触发崩溃的场景。
  • 3(SRV_FORCE_NO_TRX_UNDO):不执行事务回滚。适合回滚阶段崩、undo 日志损坏导致起不来的情况。注意这意味着未提交事务不会被回滚,数据可能不干净。
  • 4(SRV_FORCE_NO_IBUF_MERGE):不合并 change buffer。适合插入缓冲损坏导致的崩溃。
  • 5(SRV_FORCE_NO_UNDO_LOG_SCAN):启动时不扫描 undo log。级别很高了,跳过这一步意味着 InnoDB 会把未提交事务当已提交,风险明显上升。
  • 6(SRV_FORCE_NO_LOG_REDO):不执行 redo 前滚。这是最后一招,用它会跳过崩溃恢复的全部重做,数据一致性无法保证,只求能起来把数据导出来。

用法建议:从 1 开始,起不来再加到 2,依次往上。不要一上来就用 6。每改一次级别都要重启 mysqld。注意 innodb_force_recovery > 0 时整个库是只读的,无法 INSERT/UPDATE,也无法正常 SHUTDOWN 之外的写操作——这是设计使然,因为你正在抢救,不该再写入。

第三步:能起来就赶紧导数据,别贪恋原地修复

抢救模式下的第一优先级不是"修好表继续用",而是把数据导出到干净的实例或 SQL 文件。因为抢救模式下运行本身就不稳定,多待一分钟多一分风险。导出方式:

# 逻辑导出,只导结构+数据,不带建库语句,方便灌到别处
mysqldump --single-transaction --skip-lock-tables \
  --no-create-db your_db your_table \
  > /backup/your_table.sql 2> /backup/dump.err

# 如果某行数据本身已损坏,加下面两个参数跳过坏行
mysqldump --force --ignore-error=1064 ...

遇到 mysqldump 中途报 Lost connection,通常是读到坏页把连接打挂。这时可以按主键范围分片导,把损坏区间绕过去:

# 先看主键范围
SELECT MIN(id), MAX(id) FROM your_table;
# 分批导出,避开已知会挂的区间
mysqldump via --where="id BETWEEN 1 AND 100000" ...

把能导出来的都导出来、灌回一个干净的、同版本的 MySQL 实例,然后这张坏表就放弃救援、重建即可。这是 95% 的情况下的最优解——数据比表结构值钱,别为了保全一张物理表搭进整个库。

第四步:MySQL 彻底起不来时,用 innodb_force_recovery 也无效怎么办

有几种硬骨头,force_recovery 也救不动:系统表空间(ibdata1)损坏、数据字典损坏。这时只能上"物理抽取"工具。首选 ibd2sdi(MySQL 8.0 自带)和 innochecksum 做健康检查:

# 检查某个 ibd 文件的页校验,定位损坏页
innochecksum --page-type-dump=/tmp/pages.txt /var/lib/mysql/your_db/your_table.ibd

# 从 ibd 中导出所有表定义(SDI),用于重建表结构
ibd2sdi /var/lib/mysql/your_db/your_table.ibd | jq . > /tmp/table_sdi.json

如果整个 MySQL 起不来、又想强行把 ibd 里的数据拉出来,有一个经典技巧:在一个全新的、干净的 MySQL 实例里,建一张结构完全一致的空表(从 SDI 或老备份里拿 CREATE TABLE 语句),然后 discard 掉它的表空间,把损坏的 .ibd 拷进去再 import:

-- 在干净实例里
CREATE TABLE your_db.your_table (...);      -- 结构与原来完全一致
ALTER TABLE your_db.your_table DISCARD TABLESPACE;
# 拷文件(此时 MySQL 需停止该表的写,最好停库)
cp /var/lib/mysql.bak.*/your_db/your_table.ibd /var/lib/mysql/your_db/
chown mysql:mysql /var/lib/mysql/your_db/your_table.ibd
-- 回来 import(MySQL 8.0 需要对应的 .cfg;老版本直接 import 即可)
ALTER TABLE your_db.your_table IMPORT TABLESPACE;

这一步对页损坏有"过滤"效果:IMPORT 会重新校验每个页,坏的页可能直接报错,但读得出来的数据通常能进来。配合前面 force_recovery=1/3 的组合使用,成功率不低。如果 import 报页错,再考虑把 force_recovery 调到更高重试。

哪些情况必须放弃救援

以下情形,真别硬救了,直接上备份:

  • 备份是最近的、可靠的。有可用备份时,恢复备份永远是最快最安全的选择,别为省几小时赌上整库。
  • 损坏的是系统表空间/数据字典且没有干净副本。这种物理修复基本没有民间手段。
  • 涉及磁盘硬件故障。同一块盘上继续读写只会扩大损坏面,先换盘。

所以,本文真正想传递的观念其实是:恢复的最后一道防线永远是备份,而不是修复技术。会修不如有备。

预防:让"损坏"这件事尽量不致命

  • 开启并验证双一配置。 innodb_flush_log_at_trx_commit=1 和 sync_binlog=1,这是崩溃安全的基础,别为了性能把它们改成 0 或 2 还自我安慰。
  • 备份要能恢复才算备份。 定期做恢复演练:找一台闲置机器,从备份灌一遍,确认数据完整。只备份不验证,等于没备份。
  • 开启 innodb_file_per_table。 单表独立表空间让单表损坏可控,也方便单独拷表。
  • 物理备份 + 逻辑备份双保险。 mariabackup/xtrabackup 物理热备快、便于整库恢复;mysqldump 逻辑备份便于跨版本、跨机器、选择性恢复。
  • 监控磁盘和 SMART。 坏道往往先有征兆,SMART 告警和内核日志里的 I/O error 要盯着。
  • 重要库做异机/异地副本。 本地盘再稳也挡不住整机报废,异地副本是最后保险。

InnoDB 损坏听起来吓人,但拆开看就是"判断范围 → 冷备 → force_recovery 逐级启动 → 导数据回干净实例"。记牢这个顺序,遇到时就不会乱。真正决定生死的,是你在出事之前有没有一份验证过的备份。趁现在没事,去恢复演练一遍,比任何后悔都值钱。

Last modification:October 8th, 2026 at 07:33 pm

Leave a Comment