马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
MySQL 误删数据恢复实战:从 binlog 定位到闪回重建
一、具体的问题
周五下午四点多,运营同事反馈订单列表少了大半天的数据。排查发现,一个新上线的数据订正脚本条件写错,把 orders 表里 create_time > '2026-10-09' 误写成了 create_time > '2026-09-09',一条不带 LIMIT 的 UPDATE 把近一个月的状态字段全刷成了同一个值,影响约 18 万行。更麻烦的是,业务在这之后还在持续写入——也就是说,脏数据和好数据已经混在一起了。
当时的恢复诉求很明确:不能整库回滚到昨天(会丢掉今天全部正常业务),只把这张表这批被误改的行恢复回去,而且停机时间越短越好。最终我们靠「全量备份 + binlog 重放 + 闪回误操作事务」的组合在 40 分钟内完成,一条数据没丢。这套流程拆开讲。
二、核心原理
误删/误改数据的恢复,底层只有一条路:全量备份恢复出一个"干净的过去",再用 binlog 把这段过去之后的正常变更重放回来,最后把误操作那一个事务逆向抵消掉。
binlog 闪回的前提常被忽视:binlog_format=ROW 且 binlog_row_image=FULL 时,binlog 里记录了每行变更的前镜像(改之前的值)和后镜像。闪回工具(my2sql、binlog2sql 等)就是拿前镜像生成逆向 SQL。如果还是 STATEMENT 格式,binlog 里只有 UPDATE ... WHERE ... 这条语句本身,没有每行的旧值,闪回就无从谈起——很多老库默认还是 STATEMENT,出事那天才发现就晚了。
另一个关键区分:DELETE/UPDATE 这类 DML 可以闪回,但 DROP TABLE、TRUNCATE 是 DDL,binlog 里只记一条语句,没有任何行数据。DDL 误操作只能走全量恢复 + 重放到 DDL 执行前,这条路必须提前有备份撑着,事后是造不出来的。
还有一条铁律:一切重放和闪回都在临时实例上做完、校验通过之后,再导回生产。 直接在主库上重放 binlog 或跑回滚 SQL,一旦中断或重放错位,主从复制立刻错乱,事故升级成双倍事故。
三、实例参考(动手步骤)
步骤 0:止血与基线确认。 先停应用写入或对库设只读,防止脏数据继续扩散:
- SET GLOBAL super_read_only = ON;
- SHOW VARIABLES WHERE Variable_name IN
- ('log_bin','binlog_format','binlog_row_image','binlog_expire_logs_seconds');
- SELECT COUNT(*) FROM orders WHERE update_time > '2026-10-09 15:00';
复制代码
三项变量只要有一项不满足(log_bin=OFF / STATEMENT / MINIMAL),闪回路就走不通,直接转全量恢复方案,别浪费时间试。同时和业务方对账受影响行数,作为恢复后的验收基准。
步骤 1:定位误操作事务。 用 mysqlbinlog 解码,找到误操作的 GTID 和起止 position:
- mysqlbinlog --no-defaults --base64-output=decode-rows -vv \
- --start-datetime='2026-10-09 15:00:00' \
- binlog.000123 | grep -B5 -A20 'UPDATE `orders`'
复制代码
记下误操作事务的 GTID(假设 GTID 3-1-88231,XID 起止 position),这是后面重放边界的依据。装有 my2sql 的环境可以直接 my2sql -mode repl 拉出人类可读 SQL,定位更快。
步骤 2:临时实例恢复全量备份。 取最近一次 XtraBackup 全备,恢复到同机的 3307 临时实例,绝不动生产:
- xtrabackup --copy-back --target-dir=/data/bak/20261009_full/ \
- --datadir=/data/mysql3307/data/
- chown -R mysql:mysql /data/mysql3307/ && systemctl start mysql3307
复制代码
步骤 3:binlog 重放到误操作前。 把备份完成时刻到误操作事务之前的所有 binlog 重放进临时实例:
- mysqlbinlog --start-position=4 --stop-position=18765544 \
- binlog.000122 binlog.000123 | mysql -h127.0.0.1 -P3307 -uroot -p
复制代码
--stop-position 一定要落在误操作事务开始之前。重放完核对临时实例的数据到了"误操作前一秒"。
步骤 4:闪回误操作事务。 用 my2sql 对误操作那段 binlog 生成逆向 SQL:
- my2sql -user repl -password 'xxx' -host 127.0.0.1 -port 3306 \
- -start-file binlog.000123 -start-pos 18765544 -stop-pos 18910222 \
- -mode file -local-dir ./flashback_out -flashback
复制代码
产物是按倒序生成的回滚 SQL。在临时实例上执行它,然后核对:被误改的 18 万行全部回到旧值,且 15:00 之后的正常业务数据原样保留。
步骤 5:单表校验后导回生产。 只导目标表,不做整库同步:
- mysqldump -h127.0.0.1 -P3307 orders --no-create-info \
- --replace --where="update_time>'2026-10-09 15:00'" > fix_orders.sql
- mysql -h生产 -uroot -p orders < fix_orders.sql
复制代码
导完按步骤 0 的口径重新 COUNT 对账,与业务方确认一致后 SET GLOBAL super_read_only = OFF,恢复写入。全程 42 分钟。
四、实操检查清单
- 日常三参数到位:log_bin=ON、binlog_format=ROW、binlog_row_image=FULL,纳入巡检
- binlog 保留时长(binlog_expire_logs_seconds)至少覆盖两个备份周期,且备份盘上另存一份
- 每日全备(或全备+增量),且每月做一次真实恢复演练——没演练过的备份等于没有备份
- 订正脚本上线前在测试库跑一遍,生产执行必须带 LIMIT 试跑 + SELECT 预览影响行数
- 应用与运营账号只授 DML 权限,DROP/TRUNCATE/ALTER 收归 DBA 账号
- 服务端 sql_safe_updates=ON,禁止无索引条件的 UPDATE/DELETE 直接执行
- 预案文档写清三件事:临时实例端口、恢复命令模板、业务对账人,出事时不用现想
几个容易踩的坑
坑一:STATEMENT 格式发现太晚。 闪回工具全部依赖行镜像,STATEMENT 下只能全量重放,恢复窗口从分钟级变成小时级。这是巡检项,不是事故项。
坑二:stop-position 落进了误操作事务中间。 重放边界差一个事务,就会把脏数据也重放进去。定位时以 XID 为界,宁可少重放一个正常事务,再单独补。
坑三:DDL 误操作指望闪回。 DROP 了表找 binlog2sql 是找不到行数据的,只能全量恢复 + 重放到 DDL 前。反过来这也说明:大表 DDL 前先手动备份一次,价值就在这里。
坑四:在主库直接跑回滚 SQL。 临时实例验证通过后再单表导回,是这条流程的底线。
坑五:binlog 已过期清理。 误操作发生在一个备份周期之前,而对应 binlog 已被清掉,中间那段就永远补不回来了。保留时长必须大于"最长可能的事故发现时间"。
治理前后对比
| 指标 | 治理前 | 治理后 | | 恢复总耗时 | 手工逐条捞 6 小时以上 | 42 分钟 | | 数据找回 | 缺 3 小时正常订单 | 0 行丢失 | | 误操作止血 | 47 分钟后才设只读 | 预案明确,5 分钟内只读 | | 恢复演练 | 从未做过 | 每月 1 次,流程走通 | | 订正脚本管控 | 直连生产随便跑 | 试跑+LIMIT+审计,双人复核 |
误删恢复这件事,功夫全在事前:ROW+FULL、备份可恢复、权限收敛、演练常态化。事故当天能做的,只是把练过的动作再走一遍。 |