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工具可以把二进制日志转成可读的SQLmysqlbinlog \ --start-datetime2025-07-22 00:00:00 \ --stop-datetime2025-07-29 14:30:00 \ --databasedb_aidpt_flowable \ --base64-outputDECODE-ROWS \ --verbose \ binlog.000009 recovery.sql关键参数说明--start-datetime/--stop-datetime精确卡出事故前7天的数据变更。时间要精确到事故操作的前一分钟--database只提取目标库的变更减少噪音--base64-outputDECODE-ROWS--verbose把ROW格式的二进制数据解码成可读的SQL伪代码跑完后recovery.sql大约 2GB里面是这7天所有的行级变更记录。第三步生成 REPLACE INTO 脚本binlog 解析出来的SQL伪代码不能直接执行。举个例子binlog输出的是### INSERT INTO db_aidpt_flowable.approval_record ### SET ### 11001 ### 2请假申请 ### 31 ### 42025-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里表示为NULLINSERT语句要对NULL做处理字符串值可能有换行符和特殊字符要加引号转义同一个事务内的多条DML按顺序生成保证一致性第四步华为云验证拿到REPLACE INTO脚本后先在华为云上跑一遍。验证三个点行数对比— 每个表的行数跟预期是否一致关键数据抽查— 挑几个已知的单据检查字段值是否完整关联完整性— 外键关联是否断裂比如审批记录关联的流程实例ID是否还能查到测试环境跑完后生成了一份验证过的恢复脚本准备上阿里云。第五步阿里云执行恢复执行前先锁库或者在低峰期操作避免恢复过程中有新写入导致数据不一致。23张表逐张执行跑完后核心业务数据全部找回。恢复后的副作用排查数据恢复不是跑完脚本就结束了。恢复过程中会产生一批新的自增ID、新的流程实例跟之前残留的旧数据撞在一起引发了一系列连带问题。排查和修复花的时间不比恢复本身少。1. 审批列表残留现象4个单据HR审批完成后仍出现在待审批列表中根因7月30日残留的flow_user、flow_task、flow_his_task表中存在del_flag0的旧记录恢复脚本没有清理修复手动清理对应流程实例的 flow 表数据2. 附件关联断裂现象部分单据的附件打不开根因单据关联了旧行程ID旧行程数据已被删除附件指向的business_sub_id失效修复更新附件的business_sub_id为新的有效ID3. 重复审批记录现象个别单据出现多条审批记录状态不一致根因旧流程实例的is_latest1标记残留与恢复后的新记录共存修复按单据的最新flow_instance重新设置is_latest4. 费用明细多出记录现象差旅报销单的费用明细比实际多出几条根因恢复脚本误复原了之前已被软删除的 detail 记录修复给恢复回来的多余记录打上is_delete15. is_latest 修复脚本缺陷现象第一次修复用了按流程实例修的逻辑导致一个单据如果有多个流程实例每个实例都留了一个is_latest1根因应该按单据维度修而不是按流程实例维度修——一个单据只有一条最新的审批记录修复改为按单据分组只保留最新的一条is_latest16. 权限查询Bug现象A0021 用户搜自己看到的却是别人的单据根因查询权限分了自己创建的和自己审批的两条路径。代码里取initiatorId用的是creator字段但实际creator存的是审批人而不是提交人修复改为从flow_instance.create_by取提交人7. 已完成列表显示异常现象已完成Tab为空全部Tab里已完成单据显示的当前节点为nullfallback 到了历史的 nodeName根因allList查询缺少flowStatus判断逻辑已完成单据的当前节点字段为空时直接读了历史节点名修复加1.equals(flowStatus)判断流程状态为已完成时特殊处理8. 差旅报销单摘要格式不统一现象新旧单据在列表里的摘要格式不一致旧的只有姓名 [差旅费] 金额缺少出差事由根因恢复后的老数据摘要模版与当前代码生成的格式不同修复统一为姓名 金额 事由格式经验总结备份 cron 必须放在 root 下— 之前图省事放ecs-user用sudo跑。但 cron 非交互环境不读终端输入sudo要密码凌晨4点的备份任务默默失败了半年直到出事才发现。已迁移到 root cronbinlog 保留时间越长越好— 这次能恢复全靠 binlog 覆盖了 7 天。默认的expire_logs_days7是底线有条件设 14 天恢复先在测试环境验证— 别拿到脚本就上生产。用相同版本的 MySQL 跑一遍对比行数、抽查数据恢复后的副作用不要低估— 主数据恢复了但关联的附件、审批记录、历史缓存可能全乱套。恢复后的排查工作量常常超过恢复本身恢复脚本用 REPLACE INTO— 幂等性好不怕重复执行。直接 INSERT 会报唯一键冲突出事后先冷静定位别急着操作— 第一步不是还复什么救什么是先确认 binlog 还在不在。一旦 binlog 被轮转覆盖就真没了我正用一个人AI模式做独立开发和运维更多实战经验https://gitee.com/yao113088/jiguang-dev