MySQL误删23张表后的24小时:binlog恢复实战

📅 2026/8/1 12:17:01 👁️ 阅读次数 📝 编程学习
MySQL误删23张表后的24小时:binlog恢复实战

背景

周二下午,生产环境一条误操作,阿里云上db_aidpt_flowable库 129 张表全部被清空。

好消息是有7天前的全量备份。其中 106 张是老系统遗留的 ACT_ 表,这7天没有新数据,备份直接还原即可。真正头疼的是另外 23 张业务表——这一周里用户提交了几十条审批、上百条工作记录,全在这23张表里,没了任何一个都交不了差。

这篇文章复盘从发现事故到完整恢复的全过程:怎么用 binlog 精确找回7天的增量数据、怎么验证、怎么处理恢复后的副作用。

恢复思路

手里有三样东西:

  • 本地7月22日全量备份— 7天前的完整数据

  • 服务器 binlog.000009— 从3月覆盖到7月30日,包含这7天所有的 INSERT/UPDATE/DELETE

  • 一台华为云测试机— 跟阿里云同版本MySQL,可以先验证再上线

思路很明确:把7月22日备份还原到测试机 → 从 binlog 提取 7/22 到 7/29 事故前的增量 → 在测试机重放验证 → 确认无误后上阿里云执行。

第一步:还原全量备份到测试环境

先把7月22日的备份在华为云上恢复出来。这一步没难度,唯一的坑是确认测试机的MySQL版本、字符集跟阿里云完全一致,否则后面 binlog 重放直接报错。

mysql -h127.0.0.1 -uroot -p < backup_20250722.sql

第二步:提取 binlog 增量

这才是核心。mysqlbinlog工具可以把二进制日志转成可读的SQL:

mysqlbinlog \ --start-datetime="2025-07-22 00:00:00" \ --stop-datetime="2025-07-29 14:30:00" \ --database=db_aidpt_flowable \ --base64-output=DECODE-ROWS \ --verbose \ binlog.000009 > recovery.sql

关键参数说明:

  • --start-datetime/--stop-datetime:精确卡出事故前7天的数据变更。时间要精确到事故操作的前一分钟

  • --database:只提取目标库的变更,减少噪音

  • --base64-output=DECODE-ROWS+--verbose:把ROW格式的二进制数据解码成可读的SQL伪代码

跑完后recovery.sql大约 2GB,里面是这7天所有的行级变更记录。

第三步:生成 REPLACE INTO 脚本

binlog 解析出来的SQL伪代码不能直接执行。举个例子,binlog输出的是:

### INSERT INTO `db_aidpt_flowable`.`approval_record` ### SET ### @1=1001 ### @2='请假申请' ### @3=1 ### @4='2025-07-25 10:30:00'

需要转成:

REPLACE INTO approval_record(id, title, status, create_time) VALUES(1001, '请假申请', 1, '2025-07-25 10:30:00');

为什么用REPLACE INTO而不是INSERT INTO

因为 binlog 里既有 INSERT 也有 UPDATE 和 DELETE。REPLACE INTO 的逻辑是"有则覆盖,无则插入",正好覆盖这三种情况。如果数据已存在(来自7月22日备份的),就更新为最新版本;如果不存在,就插入。

写脚本解析 binlog 文本时注意几个点:

  • ROW格式下@1@2这类占位符对应表的列序号,需要查INFORMATION_SCHEMA拿到列名

  • NULL值在binlog里表示为NULL,INSERT语句要对NULL做处理

  • 字符串值可能有换行符和特殊字符,要加引号转义

  • 同一个事务内的多条DML,按顺序生成保证一致性

第四步:华为云验证

拿到REPLACE INTO脚本后,先在华为云上跑一遍。验证三个点:

  1. 行数对比— 每个表的行数跟预期是否一致

  2. 关键数据抽查— 挑几个已知的单据,检查字段值是否完整

  3. 关联完整性— 外键关联是否断裂(比如审批记录关联的流程实例ID是否还能查到)

测试环境跑完后,生成了一份验证过的恢复脚本,准备上阿里云。

第五步:阿里云执行恢复

执行前先锁库(或者在低峰期操作),避免恢复过程中有新写入导致数据不一致。23张表逐张执行,跑完后核心业务数据全部找回。

恢复后的副作用排查

数据恢复不是跑完脚本就结束了。恢复过程中会产生一批新的自增ID、新的流程实例,跟之前残留的旧数据撞在一起,引发了一系列连带问题。排查和修复花的时间不比恢复本身少。

1. 审批列表残留

现象:4个单据HR审批完成后仍出现在待审批列表中

根因:7月30日残留的flow_userflow_taskflow_his_task表中存在del_flag=0的旧记录,恢复脚本没有清理

修复:手动清理对应流程实例的 flow 表数据

2. 附件关联断裂

现象:部分单据的附件打不开

根因:单据关联了旧行程ID,旧行程数据已被删除,附件指向的business_sub_id失效

修复:更新附件的business_sub_id为新的有效ID

3. 重复审批记录

现象:个别单据出现多条审批记录,状态不一致

根因:旧流程实例的is_latest=1标记残留,与恢复后的新记录共存

修复:按单据的最新flow_instance重新设置is_latest

4. 费用明细多出记录

现象:差旅报销单的费用明细比实际多出几条

根因:恢复脚本误复原了之前已被软删除的 detail 记录

修复:给恢复回来的多余记录打上is_delete=1

5. is_latest 修复脚本缺陷

现象:第一次修复用了"按流程实例修"的逻辑,导致一个单据如果有多个流程实例,每个实例都留了一个is_latest=1

根因:应该按单据维度修,而不是按流程实例维度修——一个单据只有一条最新的审批记录

修复:改为按单据分组,只保留最新的一条is_latest=1

6. 权限查询Bug

现象:A0021 用户搜自己看到的却是别人的单据

根因:查询权限分了"自己创建的"和"自己审批的"两条路径。代码里取initiatorId用的是creator字段,但实际creator存的是审批人而不是提交人

修复:改为从flow_instance.create_by取提交人

7. 已完成列表显示异常

现象:"已完成"Tab为空,"全部"Tab里已完成单据显示的当前节点为null,fallback 到了历史的 nodeName

根因allList查询缺少flowStatus判断逻辑,已完成单据的当前节点字段为空时直接读了历史节点名

修复:加"1".equals(flowStatus)判断,流程状态为"已完成"时特殊处理

8. 差旅报销单摘要格式不统一

现象:新旧单据在列表里的摘要格式不一致,旧的只有"姓名 [差旅费] 金额",缺少出差事由

根因:恢复后的老数据摘要模版与当前代码生成的格式不同

修复:统一为"姓名 金额 事由"格式

经验总结

  1. 备份 cron 必须放在 root 下— 之前图省事放ecs-user,用sudo跑。但 cron 非交互环境不读终端输入,sudo要密码,凌晨4点的备份任务默默失败了半年,直到出事才发现。已迁移到 root cron

  2. binlog 保留时间越长越好— 这次能恢复全靠 binlog 覆盖了 7 天。默认的expire_logs_days=7是底线,有条件设 14 天

  3. 恢复先在测试环境验证— 别拿到脚本就上生产。用相同版本的 MySQL 跑一遍,对比行数、抽查数据

  4. 恢复后的副作用不要低估— 主数据恢复了,但关联的附件、审批记录、历史缓存可能全乱套。恢复后的排查工作量常常超过恢复本身

  5. 恢复脚本用 REPLACE INTO— 幂等性好,不怕重复执行。直接 INSERT 会报唯一键冲突

  6. 出事后先冷静定位,别急着操作— 第一步不是"还复什么救什么",是先确认 binlog 还在不在。一旦 binlog 被轮转覆盖,就真没了


我正用"一个人+AI"模式做独立开发和运维,更多实战经验:https://gitee.com/yao113088/jiguang-dev