MySQL误删数据后用binlog恢复的方法(mysqlbinlog定位时间点、ROW格式闪回与my2sql生成回滚SQL)

一条DELETE忘了带WHERE,或者UPDATE把整表状态刷了,备份是昨晚的,中间几小时的数据就没了。只要binlog还在,这类误操作基本能捞回来。真正决定成败的是事发头几分钟的动作。

先停写,别做多余操作

发现误删的第一件事是掐掉写入:业务侧停服,或者SET GLOBAL read_only = ON顶上。千万不要重启MySQL,千万不要FLUSH LOGS,千万不要再跑任何写操作。重启和FLUSH LOGS都会触发日志轮转,正在写的那个文件被封口,误操作前后连在一起的证据链就断了一截;继续写入则会把新数据混进来,之后按位置切分时会分不清哪些是要保留的。

停稳之后再看文件还在不在:

SHOW BINARY LOGS;

这条在MySQL 8.4.0之前叫SHOW MASTER LOGS,8.4.0里旧写法直接报语法错误,新名字是SHOW BINARY LOGS。同理,SHOW MASTER STATUS改成了SHOW BINARY LOG STATUS,PURGE MASTER LOGS改成了PURGE BINARY LOGS,这批复制相关的术语在8.4.0是一次性换掉的,老脚本上去就废。

三个前提,缺一个就换方案

SELECT @@log_bin, @@binlog_format, @@binlog_row_image, @@binlog_expire_logs_seconds;

log_bin是ON、binlog_format是ROW、binlog_row_image是FULL,三条都满足才能做行级闪回。MySQL 8.0起binlog默认就是开着的,binlog_format从5.7.7起默认ROW,所以新装的库通常都是这个组合。

binlog_format是STATEMENT或MIXED时,日志里存的是SQL文本,不是行数据,闪回工具拿不到旧值,只能靠全量备份加增量回放。binlog_row_image是MINIMAL时,前镜像只留主键,非主键列救不回来,恢复出来的行是残缺的。

还有一条容易忽略:误删之后这张表不能做过DDL。表结构一变,日志里的列序号就对不上现在的字段,解析出来的回滚语句会串列。

把误操作的那一段圈出来

先按时间粗筛。知道大概几点出的事,就先用时间窗口切一刀:

mysqlbinlog --start-datetime='2026-10-30 14:20:00' --stop-datetime='2026-10-30 14:35:00' \
  --base64-output=DECODE-ROWS -v --database=shop \
  /var/lib/mysql/binlog.000137 > /tmp/op.sql

--base64-output=DECODE-ROWS加上 -v是把ROW事件翻译成人能读的伪SQL,少了这两个参数只会看到一行行base64。输出长这样:

### DELETE FROM `shop`.`orders`
### WHERE
###   @1=10237
###   @2='2026-10-12 09:11:03'
###   @3=289.00

再按位置精确定位。每一段开头有 # at数字,结尾有end_log_pos数字,取误删事务的起点和终点两个位置记下来,后面的工具都靠这两个数。用SHOW BINLOG EVENTS也能看,但输出里没有行内容,只适合找位置。

有全量备份时:备份回档加日志重放

这是最干净的一条路。先把昨晚的全备恢复到一个临时实例,再把备份时刻到误删前那一刻的binlog重放上去。备份时记下的那个binlog文件名与位置就是起点,误删事务的起点就是终点:

mysqlbinlog --start-position=4 --stop-position=377308574 \
  binlog.000136 binlog.000137 | mysql -uroot -p

跨文件时按顺序把文件名都列上,别只给一个。重放结束后核对行数与时间,确认无误再导回生产。之前遇到过一次,备份其实已经坏了三个月没人发现,全靠binlog撑着,所以备份可用性得定期验,不能只看着任务成功。

只想把误操作那段撤掉:用闪回工具

没有可用备份,或者只想回滚那几分钟的操作、保留之后的写入,就得把DELETE反转成INSERT、UPDATE反转成反向UPDATE。手工干不现实,三类工具按环境选:

MySQL 8.0与8.4用my2sql,Go写的,能离线解析拷贝出来的binlog文件,不打扰主库。命令形如:

./my2sql -user u_rollback -password 'xxx' -host 127.0.0.1 \
  -databases shop -tables orders \
  -work-type rollback \
  -start-file binlog.000137 -start-pos 377308000 -stop-pos 377308676 \
  -output-dir ./recover01

生成的recover01/rollback.*.sql就是反向语句。注意它连库时用的账号要有SELECT、REPLICATION SLAVE、REPLICATION CLIENT权限,而且MySQL 8.0上这个账号得是mysql_native_password认证的,用caching_sha2_password会连不上。

5.6、5.7环境用binlog2sql,Python写的,加 -B参数直接输出回滚SQL。放到MySQL 8.0上要先把binlog_row_metadata设成FULL,这个变量默认是MINIMAL,不调整的话解析结果里没有列名,回滚语句拼不出来。

要的是binlog格式的回滚文件、或者日志体量特别大时,用美团开源的MyFlash,C写的,解析速度最快,产出的是一个可以直接用mysqlbinlog回放的文件。

生成完别急着执行

回滚SQL出来之后,先找一台临时实例或从库回放,核对影响行数与抽样数据,确认之后再动生产库。现场见过的翻车方式基本都是跳过了这一步:回滚语句没限定表,把别的表也一起改了。

开着GTID的库还有个坑。同一个GTID在这个实例上已经执行过,直接回放会被静默跳过,一条都不生效。回放时要加 --skip-gtids让mysqlbinlog不输出GTID事件:

mysqlbinlog --skip-gtids --include-gtids='3e11fa47-71ca-11e1-9e33-c80aa9429562:1-100' \
  binlog.000137 | mysql -uroot -p

回放前在会话里SET sql_log_bin=0,避免这次恢复动作本身又写进binlog、顺着复制扩散到从库。恢复完记得改回来。

TRUNCATE和DROP救不回来

这两个是DDL,binlog里只记一条语句,没有任何行数据,闪回工具对它们无能为力。碰到这种情况只能走全量备份加增量重放,没有备份就是真没了。这也是为什么生产库的DROP权限要单独收着。

平时留够余量

binlog的保留期由binlog_expire_logs_seconds控制,默认2592000秒,也就是30天,到期的文件会被自动清掉;expire_logs_days这个旧变量在8.4.0已经被移除。误删隔了一周才发现、日志只剩三天的库,实测不止一次,所以保留期别设太短,磁盘够就留7天以上。手工清理用PURGE BINARY LOGS TO 'binlog.000150',别在操作系统里直接rm,那样MySQL的索引文件对不上。

两道便宜的保险:给业务账号的会话开sql_safe_updates,不带WHERE或者不带KEY条件的UPDATE、DELETE会被直接拒绝;有条件的再挂一台延迟从库,CHANGE REPLICATION SOURCE TO SOURCE_DELAY=3600(8.0里叫MASTER_DELAY),相当于常备一个一小时前的自己,误操作之后直接从它身上取数据,比解析binlog省事得多。

版本相关的行为差异以官方文档为准:8.4.0是2024年4月发布的LTS,8.0系列已于2026年4月停止支持,9.7.0是2026年4月发布的下一个LTS。数据截至2026年10月。