面试官推过来一张纸,上面只有一行字:

DELETE FROM operation_log WHERE create_time < '2024-01-01';

他敲了敲桌子:"你本想清掉三个月前的日志,SQL都写好了,复制粘贴进终端时,手一抖,WHERE那一行没粘进去。你直接回车了。

实际执行的,是这行:

DELETE FROM operation_log;

表里有10亿条数据,主从同步,没有延迟从库。现在,拿什么救命?"

我手心冒汗。这是字节二面,不是背八股,是现场解题。

一、第一回合:现场评估——先止血,再救人

面试官:第一步做什么?

先停应用写入,把主库的写流量切走或限流,防止新数据覆盖旧数据的binlog位置。然后确认三件事:

  1. 表结构:有没有主键?有没有外键约束?有没有触发器?(这些会影响闪回后数据一致性)

  2. binlog状态:格式是ROW还是STATEMENTexpire_logs_days还剩几天?

  3. 备份窗口:最近一次全量备份是昨晚几点?能不能接受丢数据?

面试官:如果binlog是STATEMENT格式呢?

:那这道题直接判死刑。STATEMENT只记录SQL文本,那条DELETE在binlog里就是一行字符串,没有行级镜像,任何闪回工具都无能为力。只能拿昨晚的全备,叠加上备份点之后的binlog做增量恢复,业务至少丢几小时数据,而且恢复期间必须停服。

面试官:你们线上用ROW还是STATEMENT

必须是ROWSTATEMENT省的那点磁盘空间,抵不上一次误删的代价。而且ROW格式下,binlog会记录每行删除前的完整镜像,这是后面所有操作的前提。

二、第二回合:binlog闪回——和时间赛跑

面试官:确认是ROW格式了,现场写恢复命令。

:用binlog2sqlMyFlash。以binlog2sql为例:

# 1. 先定位误删的binlog文件和位置mysqlbinlog --start-datetime="2024-04-01 14:00:00"\ --stop-datetime="2024-04-01 14:05:00"\ mysql-bin.000045 | grep -i "DELETE FROM operation_log"
# 2. 解析生成反向SQL(DELETE转INSERT)python binlog2sql.py -h127.0.0.1 -P3306 -uadmin -p'xxx' \ -d operation_db -t operation_log \ --start-file='mysql-bin.000045' \ --start-pos=123456\ --stop-pos=234567\ -B > rollback.sql

-B参数就是flashback模式,把DELETE事件反向生成INSERT

面试官:10亿行数据,生成反向SQL要多久?直接执行rollback.sql吗?

不能直接执行。10亿行的INSERT文件可能有几百GB,直接灌回去会打爆主库IO,触发从库延迟,甚至把业务拖垮。

正确做法是:

  1. 分段处理:按主键范围切分,比如每批100万行,用LIMITWHERE id BETWEEN分批回插。

  2. 先导入临时表

    CREATE TABLE operation_log_recovery LIKE operation_log
    ,把闪回数据先写进临时表,校验行数、抽样比对关键字段,确认无误后再做RENAME TABLEINSERT ... SELECT合并。
  3. 低峰期操作:如果业务能接受,凌晨执行;如果不能停服,临时表数据补完后,用pt-table-sync或主键对比的方式增量补齐操作期间的新增数据。

面试官:如果binlog刚好第8天被清理了呢?

:这就看公司的expire_logs_days配置了。字节这种体量,binlog一般会保留7-15天,且会异地归档到对象存储(比如S3或内部HDFS)。如果本地被清理但归档还在,下载回来解析就行;如果连归档都没有,那只能回退到全量备份+增量binlog的恢复路径,丢数据是肯定的

面试官:闪回期间,业务要不要停服?

:理想情况不停。但如果原表有自增主键唯一索引,反向插入时可能和新写入的数据冲突。这时候有几种策略:

  • 临时表恢复后,把冲突区间的自增值手动跳过;

  • 或者短暂停写(分钟级),完成数据合并后再开放;

  • 最稳妥的是:先切到从库读,主库只留恢复操作。

三、第三回合:加试题——如果不止MySQL呢?

面试官:你们公司还有PG和Redis,假设同样的误操作发生在PostgreSQL和Redis,分别怎么救?

PostgreSQL:PG没有binlog,靠WAL(Write-Ahead Log)+ 归档做PITR(Point-in-Time Recovery)。需要提前有pg_basebackup的全量基准备份,再加上连续的归档WAL。误删后创建recovery.conf,设置recovery_target_time到误操作前1秒,启动实例重放WAL。但PG的PITR是整库级的,不像MySQL能单表闪回。如果只想恢复一张表,得先恢复到临时实例,再把那张表pg_dump出来灌回去。

Redis:如果开了AOF(appendonly=yes),AOF文件是纯文本指令序列。找到那条DELFLUSHALL命令,直接编辑AOF文件删掉它,重启Redis重新加载即可。如果开了RDB,只能用最近一次快照恢复,快照之后的数据全丢。Redis 4.0+的混合持久化(RDB全量+AOF增量)也是优先加载AOF,思路一样。

面试官:PG和Redis的恢复,哪个更麻烦?

PG更麻烦。Redis的AOF是文本,肉眼能改;MySQL的binlog有成熟工具;PG的WAL是二进制物理日志,必须走整库PITR,周期长、空间大、对运维要求高。

四、第四回合:没有备份——从"技术题"变成"送命题"

面试官:假设你们公司很穷,没有全备,binlog也没归档,还有救吗?

:技术上还有最后一根稻草——磁盘级数据恢复。InnoDB的.ibd文件即使被DROP,只要磁盘扇区没被新数据覆盖,可以找专业公司扫描底层、按页解析IBD结构。但按MB收费,10亿行的表恢复可能要几万到十几万,且不保证完整。

这时候问题已经从"技术问题"变成了"商务问题":是花大钱抢救,还是接受业务停摆?

面试官:结论?

备份不是可选项,是必选项。没有备份的恢复叫"赌博",而且十赌九输。

五、第五回合:预防措施——真正厉害的工程师,不让事故发生

面试官:如果让你设计一套机制,保证这类事故不发生,你怎么做?

:三层防线:

1. 事前拦截

  • 生产账号只读,DML走工单系统(Archery/Yearning),SQL必须双人复核。

  • 危险命令(DROPTRUNCATEDELETEWHERE)直接拦截,正则匹配到就拒绝执行。

  • 开发环境用sql_safe_updates强制要求UPDATE/DELETE必须带WHERELIMIT

2. 事中保护

  • 操作前EXPLAIN确认影响行数;大表操作分批+事务包裹。

  • 核心库必须配延迟从库MASTER_DELAY = 3600),这是最后的后悔药。

  • 变更窗口期放在低峰期,操作前先做CREATE TABLE ... SELECT备份当前表。

3. 事后兜底

  • 每天全备 + binlog/WAL归档,且必须做恢复演练。很多公司备份做了三年,真用时发现备份文件损坏或依赖的binlog已被清理,等于没备。

面试官合上笔记本,说了一句:

"技术救急是下限,规范流程是上限。真正厉害的工程师,不是删了数据能恢复的人,而是让删错数据这件事根本不容易发生的人。"

我深吸一口气。这题,我活下来了。

打开网易新闻 查看精彩图片