Skip to content

MySQL 误删恢复:从全量备份到时间点恢复 ​

备份任务显示成功,只能证明生成了一个文件。只有在隔离环境里把它恢复出来、重放 binlog 到正确的位置、再用业务规则校验通过,才能证明这是一个能用的恢复方案。

先看一次演练的时间线。订单库每 20ms 写入一笔合法订单,另一个会话误执行了一条删除:

text
t0  全量逻辑备份完成            订单 1—1000
t1  正常写入                     订单 1001—1200
t2  误删 DELETE FROM order_items WHERE order_id > 0,删掉 3,675 行   GTID …:1239
    误删之前已提交订单 1201—1225;与误删同一秒的有订单 1217—1250,其中 1226—1250 在它之后
t3  误删之后的合法写入           订单 1226—1250
t4  发现问题

目标不是把数据库恢复到「最新」,因为最新状态里明细已经被删了;也不是简单地恢复到 t2 之前,那样会丢掉之后的 25 笔合法订单。正确的结果是:**误删那一笔不生效,其他所有写入都在。**本文用 MySQL 8.4 自带的能力完成这次恢复,并用一个没有发生误删的对照实例证明结果正确。

一、先说结论 ​

  • 恢复 = 全量备份 + binlog 重放。 备份只能把数据带回备份那一刻,之后的写入要靠 binlog;两者缺一不可,binlog 的保留期必须覆盖备份间隔。
  • 用 GTID 截断,不用墙上时间。 实测误删所在的那一秒里有 34 笔合法订单,其中 25 笔在误删之后提交;按秒截断无论怎么选都会切错。按 GTID 可以精确停在误删之前,并跳过它继续重放。
  • 恢复要在全新的隔离实例上做。 不在原库上试错,也不让恢复实例连到生产网络。
  • 每一层成功都不等于恢复正确。 实测损坏一个字节的备份导入时退出码为 0;缺一个 binlog 文件时重放不报错。前者靠文件校验和发现,后者靠 GTID 连续性和业务校验和发现。
  • replica、延迟副本和 Clone 都不是备份。 误操作会被复制过去;延迟副本提供的是发现窗口;Clone 复制的是当前状态。

二、先定义恢复目标 ​

目标问题本次演练
RPO最多允许丢失哪些写入0:误删之外的合法写入全部恢复
RTO从开始恢复到业务可用要多久分三段记录:可读、可校验、可切换
恢复粒度实例、库、表还是业务对象整个 shop 库
正确性怎样证明恢复结果是对的与对照实例的业务校验和逐表比对,并检查业务不变量

业务不变量是最容易被忽略的一项。演练使用一个小型订单模型:订单、订单明细、库存、操作标记。每笔下单是一个事务,写一张订单、三条明细、扣三次库存、记一条连续编号的标记。于是有四条可以直接用 SQL 检查的规则:

  • 每张订单的金额等于它的明细金额之和;
  • 每个 SKU 的库存等于初始库存减去所有明细数量;
  • 操作标记的编号从 1 开始连续;
  • 没有找不到订单的明细。

误删之后,source 上 1,225 张订单的金额与明细不符,100 个 SKU 的库存与明细对不上。行数和表是否存在都查不出这类问题。

三、逻辑备份、物理备份与 Clone ​

方式得到什么适合需要注意
逻辑备份(mysqldump、MySQL Shell dump)SQL 或数据文件中小数据量、跨版本迁移、只恢复部分库表恢复要重新执行 SQL、重建索引,数据量大时很慢
物理备份(MySQL Enterprise Backup、Percona XtraBackup)数据文件的一致性拷贝大数据量、要求恢复快与版本、平台关系紧密;恢复单表更复杂
Clone 插件一个实例当前状态的完整复制快速创建 replica、搭建测试实例只有当前状态,没有历史;远程 Clone 会覆盖接收端的数据
延迟副本(SOURCE_DELAY)落后固定时间的 replica给误操作留出发现窗口窗口过去就会执行同样的误操作;它本身也可能故障

这些方式都能成为恢复体系的一部分,但真正的备份需要满足:保留历史版本、与生产隔离(权限和存储都分开)、定期恢复验证。本次演练使用 mysqldump:

bash
mysqldump --single-transaction --source-data=2 --set-gtid-purged=ON \
          --routines --triggers --events --databases shop > full.sql
  • --single-transaction 在一个一致性快照里导出 InnoDB 表,不阻塞写入;
  • --source-data=2 以注释形式记录备份对应的 binlog 文件和位置;
  • --set-gtid-purged=ON 写入 SET @@GLOBAL.GTID_PURGED,恢复实例导入后就知道哪些事务已经包含在备份里。

备份完成后记录退出码、文件大小和 SHA-256,一起存入备份清单。binlog 也要按同样的方式归档,每个文件都记录校验和。

四、在隔离实例上恢复并按 GTID 重放 ​

t0 全量备份订单 1—1000t1 正常写入1001—1200t2 误删GTID :1239t3 合法写入误删之后 25 笔t4 发现导入备份 → 重放 → UNTIL SQL_BEFORE_GTIDS停在误删之前:订单 1225 笔空事务占位跳过 :1239继续重放到 t41250 笔,与对照一致按墙上时间截断的问题:误删那一秒里还有 25 笔合法订单在它之后提交只恢复到误删之前也不对:会丢掉之后 25 笔合法订单
图 1 · 恢复 = 全量备份 + binlog 重放;按 GTID 停在误删事务之前,用空事务跳过它,再继续重放之后的合法写入

4.1 定位误操作 ​

恢复之前先找到那笔误操作的 GTID。可以从审计日志、变更系统的记录里找,也可以直接查 binlog:

sql
SHOW BINLOG EVENTS IN 'binlog.000004';
-- … Gtid        SET @@SESSION.GTID_NEXT= '…:1239'
-- … Table_map   table_id: 94 (shop.order_items)
-- … Delete_rows …

演练用脚本扫描归档的每个 binlog,找到唯一一笔删除 shop.order_items 的事务 …:1239。只凭时间戳定位是不够的:误删在 56.190 秒提交,同一秒内前后都有合法订单。

4.2 导入全量备份 ​

在一个全新的实例上:

sql
RESET BINARY LOGS AND GTIDS;   -- 导入带 GTID_PURGED 的备份前,gtid_executed 必须为空
SOURCE full.sql;

导入后先做一次基线校验:演练中四张表的业务校验和与对照实例在备份时刻完全相同。这一步证明备份本身是可用的,之后出现的差异就只可能来自重放。

4.3 用复制 SQL 线程重放 binlog ​

MySQL 8.4 的官方镜像只带 mysql、mysqldump 和 mysqlsh,没有 mysqlbinlog。演练用的是服务端自带的办法:把归档的 binlog 文件作为恢复实例的 relay log,交给复制的 SQL 线程去应用。

  1. 恢复实例启动时指定 --relay-log=restore-relay-bin 与 --skip-replica-start;
  2. 把归档的 binlog 依次复制为 restore-relay-bin.000001、000002……,写好索引文件,重启实例;
  3. 指向第一个文件,只启动 SQL 线程,停在误删之前:
sql
CHANGE REPLICATION SOURCE TO SOURCE_HOST = 'binlog-archive',
  RELAY_LOG_FILE = 'restore-relay-bin.000001', RELAY_LOG_POS = 4;
START REPLICA SQL_THREAD UNTIL SQL_BEFORE_GTIDS = '…:1239';

SOURCE_HOST 只是占位,从不启动 IO 线程。已经包含在备份里的事务在 gtid_executed 中,SQL 线程会自动跳过,所以可以放心地把全部归档文件交给它。

SQL 线程停下后检查:订单 1,225 笔,正是误删之前已提交的全部订单,四条不变量全部成立。

4.4 跳过误删,补回之后的写入 ​

用一个空事务占用误删的 GTID,再继续重放:

sql
SET GTID_NEXT = '…:1239'; BEGIN; COMMIT; SET GTID_NEXT = 'AUTOMATIC';
START REPLICA SQL_THREAD;
SELECT WAIT_FOR_EXECUTED_GTID_SET('<发现问题时 source 的 gtid_executed>', 120);
STOP REPLICA;

最终结果:订单 1,250 笔,四张表的业务校验和与对照实例完全相同,GTID 集合与 source 一致(误删的那一个以空事务占位)。

这里要注意:跳过误删是否安全取决于业务。如果误删之后的事务依赖被删除的数据,例如又基于空的明细表做了汇总,那么补回的写入也可能是错的,需要业务逐笔判断。

4.5 恢复耗时 ​

阶段从开始恢复起
可读:全量备份导入完成0.4 s
可校验:重放完成,业务校验和比对通过5.7 s
可切换:只读冒烟查询完成5.8 s

演练数据只有 1,250 笔订单,这些数字不能外推。真实环境中,逻辑备份的导入时间与数据量基本成正比,重放时间与 binlog 量成正比;恢复是否在 RTO 之内,只能用接近生产规模的数据定期演练来回答。

五、恢复后怎样证明正确 ​

① 备份文件 SHA-256与备份清单比对损坏一个字节:在这里拦下② 导入退出码只说明 SQL 执行完了损坏的备份:退出码 0③ GTID 集合完整GTID_SUBSET(目标, 已执行)缺一个 binlog:重放不报错,这里为 0④ 业务校验和与对照实例逐表比对⑤ 业务不变量金额 = 明细 · 库存守恒 · 标记连续
图 2 · 「备份成功」「导入成功」「重放没报错」都不能证明恢复正确;损坏的备份和缺失的 binlog 分别在第 1 层和第 3、4 层才被发现

演练中故意制造了两个失败路径:

备份文件损坏。 把备份文件中库存表的一个数字改掉。SHA-256 与清单不一致,恢复流程在导入前停止。如果跳过这一步强行导入,mysql 的退出码是 0,表都在、行数也对,但库存的校验和与对照不同,有 1 个 SKU 不满足库存守恒。

归档缺了一个 binlog 文件。 去掉备份之后的第一个 binlog(订单 1001—1200 所在的文件)再重放。SQL 线程没有报任何错误,把后面文件里的事务照常应用完;但 GTID 集合中缺了 …:1014-1213,GTID_SUBSET(目标集合, 已执行集合) 为 0,订单只有 1,050 笔,业务校验和与对照不同。

所以恢复后的检查要分层:

  1. 文件校验和:备份和每个 binlog 与清单一致;
  2. GTID 连续:恢复实例的 gtid_executed 包含目标集合,中间没有缺口;
  3. 业务校验和:按业务字段(不含自增 id 和时间)逐表计算,与对照或源端比对;
  4. 业务不变量:金额、库存、状态机、连续编号;
  5. 只读冒烟:应用以只读方式访问关键数据。

对照实例在真实事故中通常不存在。可以用的替代是:误操作之前的源端校验和、业务侧的对账数据,以及不依赖对照的不变量。

六、延迟副本为什么不是备份 ​

延迟副本按 SOURCE_DELAY 推迟应用事务,给了一段「误操作已经发生、副本上还没执行」的窗口。在窗口内发现问题,可以停止它的复制,用 START REPLICA UNTIL SQL_BEFORE_GTIDS 追到误操作之前,比从全量备份恢复快得多。

但它有明确的边界:

  • 窗口过去之后,误操作一样会被执行;
  • 它是另一个在线实例,可能与 source 一起故障,也在同一个权限域里,能删 source 的人通常也能删它;
  • 实测配置 SOURCE_DELAY = 1 时,实际延迟接近 2 秒,窗口的实际长度要实测,见 MySQL 故障切换 的读策略一节。

延迟副本、replica、快照和 Clone 都可以是恢复体系的一层,但都不能替代与生产隔离、保留历史、定期演练过的备份。

七、恢复演练 Runbook ​

  1. 冻结现场:停止相关写入或把受影响的表设为只读,保存误操作前后的 binlog,不在原库上试错。
  2. 定位误操作:确定 GTID、影响范围和发生时间;确认之后的写入是否依赖被破坏的数据。
  3. 准备隔离实例:全新实例、同版本,不连接生产网络。
  4. 校验并恢复全量备份:先核对 SHA-256,导入后做基线校验。
  5. 重放 binlog:检查归档文件的连续性,停在误操作之前;按业务决定跳过它、继续重放。
  6. 分层校验:GTID 连续、业务校验和、业务不变量、只读冒烟。
  7. 评审后切换:选择整体切换、只回灌部分表,或由业务补录;记录每一步的时间。
  8. 复盘:记录实际 RPO、RTO 和发现的问题,更新备份策略与演练脚本。

命令要按实际使用的备份工具编写,并在演练中反复验证,而不是事故发生时第一次执行。

八、常见误区 ​

  • 「备份任务成功了,就有备份」:没有恢复验证的备份,只是一个文件。
  • 「导入没报错就恢复好了」:实测损坏的备份导入退出码为 0。
  • 「binlog 重放不报错就是完整的」:实测缺一个文件时重放不报错,缺了 200 笔订单。
  • 「按时间点恢复到误删前一秒就行」:同一秒内前后都有合法写入。
  • 「有 replica / 延迟副本 / 快照,不用单独备份」:误操作会被复制,延迟窗口会过去,它们也和 source 在同一个权限域。
  • 「innodb_flush_log_at_trx_commit=1 就不会丢数据」:它保护的是崩溃恢复,挡不住误操作,见 Redo Log 与 Undo Log。

小结 ​

误删恢复的关键是三件事:备份和 binlog 都在,并且可以证明没有损坏、没有缺口;按 GTID 精确地停在误操作之前,并按业务决定是否补回之后的写入;恢复结果用业务校验和与不变量证明正确。这些都要在隔离实例上完成,并通过定期演练把命令和耗时固定下来。


配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权