一条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月。