1. 先搞清楚"恢复到任意时间点"到底在恢复什么
1.1 一次误操作之后,常见的是三种结局
聊 MySQL 数据恢复到任意时间点这个话题,我不想一上来就贴参数。先说说我见过最多的场面:某个业务库,凌晨跑批脚本写错了一个条件,一条不带 where 的 UPDATE 把订单状态全刷成了同一个值,等到早上业务方发现,已经过了四个小时。这时候团队的反应基本分成三档。
第一档,只有一份昨天凌晨的全量备份,没有任何增量日志。这种情况下能做的只有把库回到昨天凌晨,昨天一整天的订单、支付、库存全部蒸发,业务方只能手工补数据,补到怀疑人生。
第二档,有全量备份,也有 binlog,但是没人知道误操作发生在 binlog 的哪个位置,也没人做过重放演练。于是现场翻文档、临时搭环境、边试边错,本来两小时能搞定的事拖成了八小时。
第三档,全量备份 + 完整 binlog + 一份写清楚坐标的备份元数据 + 一台随时能起起来的隔离实例。定位、截断、重放、校验,整套动作下来一两个小时,数据只丢到误操作前的那一秒。
这篇文章想讲的就是第三档怎么搭出来。它不是某个高级特性,而是一套由全量备份、binlog 归档、位点记录、重放流程四块拼起来的工程能力。适合谁看?手上管着 MySQL、需要为数据安全兜底的后端开发、运维、DBA 都合适;对 binlog 只停留在"听说过"阶段的同学,也能从里面拿到一条能直接照着做的路径。
1.2 本质是一张照片加一卷录像带
把时间点恢复拆开看,模型特别朴素。全量备份是某个瞬间拍下的静态照片,它告诉你"在 T0 这个时刻,库里的每一行长什么样"。binlog 是 T0 之后持续录制的录像带,它记录了每一个改变数据的动作。所谓恢复到任意时间点 T,就是先把照片摆出来,再把录像带快进到 T 那个位置,逐帧播一遍。
这个模型解释了很多现象。为什么没有 binlog 就只能回到备份时刻?因为录像带断了,中间空档补不回来。为什么备份必须记录准确的起始坐标?因为录像带你得知道从第几分钟开始播,播早了重复执行,播晚了丢一段。为什么 ROW 格式这么重要?因为录像带里如果只写"把某行改成新值"这种模糊指令,回放时机器状态稍有不同,结果就不一样;而 ROW 格式写的是"这一行原来是什么、改成了什么",回放是确定性的。
我习惯把这个过程叫做"基线 + 增量重放"。理解了这一层,后面所有的参数、命令、坑,都是围绕怎么让这张照片更清晰、这卷录像带更完整、重放时的对齐更精准展开的。
1.3 三条硬前提,缺一条整条链路就废了
很多人以为开了 binlog 就等于有了时间点恢复能力,实际上至少要同时满足三个条件。
第一条,binlog 必须是开着的,而且覆盖窗口要够长。binlog 不是永久保存的,到达过期时间或者磁盘吃紧时会被自动清理。如果误操作发生在周一,而 binlog 只保留三天,周三才发现,那周一那段已经没了,神仙也救不回来。覆盖窗口要按"最晚发现误操作的时间"来定,而不是按"平均恢复时间"来定。
第二条,备份里必须带准确的起始坐标和一致的数据快照。只有数据文件没有坐标,你不知道该从 binlog 的哪个位置开始播;有坐标但数据不一致(比如备份过程中有写入),重放就会撞主键冲突或者产生重复数据。这两个问题在 mysqldump 里靠--single-transaction和--source-data解决,在物理备份里靠 redo 日志的一致性点解决。
第三条,得有一个能随便折腾的隔离环境。恢复这件事最大的风险不是技术难,而是在生产库上直接操作。拿一台独立实例,把全量恢复上去、把 binlog 重放上去、把数据校验完,确认没问题再考虑切换。没有隔离环境,很多团队会在"要不要直接在生产上试"这个问题上犹豫半小时,而这半小时里 binlog 还在继续写。
注意:第三条经常被忽略,但它决定了你出事时敢不敢动手。提前准备一台配置相近的空实例,平时就用来做恢复演练,出事时直接拿来用,这一步能省掉大量现场决策时间。
2. 底座配置:让 binlog 真正具备"可回放"的能力
2.1 binlog_format 为什么必须是 ROW,而不是 STATEMENT
这是整条链路里最不能妥协的一项。STATEMENT 格式记录的是"执行了什么 SQL 语句",看起来更省空间、更直观,但它有个致命问题:回放结果不保证和执行时一致。
举个最常见的例子,一条UPDATE t SET update_time = NOW() WHERE id = 100,STATEMENT 格式里存的就是这句原文,等你在恢复实例上重放时,NOW() 取的是重放时刻的时间,不是原始执行时刻。类似的还有UUID()、RAND()、@@session变量、带LIMIT但没ORDER BY的更新(行的命中顺序不确定)。平时主从复制看起来没事,是因为延迟小、数据分布稳定,但做恢复重放时,这些差异会实打实地变成数据错误。
ROW 格式记录的是行级前镜像和后镜像,也就是"这一行的哪几个列,从什么值变成了什么值"。回放时不需要重新求值任何函数,直接按镜像写入,结果是确定的。做时间点恢复,binlog_format=ROW是底线。
配套的还有一个参数binlog_row_image,默认是FULL,意思是前镜像和后镜像都记录完整的所有列。有人为了省空间改成MINIMAL,只记录被修改的列和主键,这在复制场景下能跑,但在反向解析回滚 SQL的场景下会缺字段,恢复出来的行是不完整的。所以我的建议是保持FULL,空间换确定性,这个交易划算。
-- 检查当前设置 SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'binlog_row_image';2.2 保留窗口怎么算:别拍脑袋,按日增量推
binlog 保留期的配置在 8.0 是binlog_expire_logs_seconds(单位秒),5.7 是expire_logs_days(单位天)。设多少合适?取决于两件事:你的 binlog 日增量,以及你能接受的"最晚发现误操作"时间。
先量日增量,方法很直接,看现有 binlog 文件的大小和生成时间:
SHOW BINARY LOGS;假设列出来一天大概生成 20GB 的 binlog,你要求覆盖 7 天,那就是 140GB 的额外磁盘占用,这部分要提前算进磁盘规划里。很多事故的真实起因不是没开 binlog,而是磁盘满了导致 binlog 写入失败,或者运维为了救磁盘手动PURGE BINARY LOGS把关键日志清了。
-- MySQL 8.0:保留 7 天 SET GLOBAL binlog_expire_logs_seconds = 604800; -- MySQL 5.7:保留 7 天 SET GLOBAL expire_logs_days = 7;顺手提一句,8.0 里binlog_expire_logs_seconds和expire_logs_days同时设置时以秒为准,两个都配上容易互相干扰,挑一个用就行。
2.3 sync_binlog:恢复精度和写入性能之间的那根绳
sync_binlog决定 binlog 什么时候真正落到磁盘。设成 1,每次事务提交都 fsync 一次,机器掉电最多丢最后一点点;设成 0,交给操作系统决定刷盘时机,性能好一些,但崩溃时可能丢掉最后几百毫秒到几秒的 binlog 事件。
这里要区分清楚两个概念:事务提交成功和binlog 落盘。如果sync_binlog=0,事务返回成功但 binlog 还在操作系统缓存里,这时候机器断电,数据页可能已经落了(innodb_flush_log_at_trx_commit=1的情况下),binlog 却丢了,这就造成了主库数据和 binlog 不一致,后面重放时会撞上"数据已存在"的错误。
我的做法是主库上sync_binlog=1,innodb_flush_log_at_trx_commit=1,两个都开,牺牲一点写入性能换数据安全。对于写入压力特别大的场景,可以退一步用sync_binlog=100这类折中值,但要清楚这意味着崩溃时可能丢掉最多 100 个事务的 binlog,时间点恢复的精度会受影响。
| 参数组合 | 崩溃时可能丢失 | 适用场景 |
|---|---|---|
| sync_binlog=1 + trx_commit=1 | 几乎不丢 | 核心业务库,恢复精度要求高 |
| sync_binlog=100 + trx_commit=1 | 最多 100 个事务的 binlog | 写入压力大、可容忍分钟级误差 |
| sync_binlog=0 + trx_commit=2 | 数秒数据 | 日志类、可重算的场景 |
2.4 GTID 和 position:两种定位方式,各有各的顺手处
定位重放起点和终点,MySQL 给了两套坐标。
基于 position 的方式,坐标形如mysql-bin.000123:884512,含义是"第 123 号日志文件的第 884512 字节"。它精确、直接,缺点是一旦日志文件被切分或者清理,坐标就失效了,而且跨主从切换后 position 完全对不上。
基于 GTID 的方式,坐标是一串uuid:transaction_id,含义是"第几个事务"。它的好处是全局唯一、跨实例可对齐,主从切换后照样能用,重放时可以指定"跳过某几个 GTID 不执行",做排除特别方便。
如果集群已经开了 GTID(gtid_mode=ON、enforce_gtid_consistency=ON),恢复流程我优先用 GTID,尤其是需要跳过个别事务的时候,比算 position 省心太多。如果没开 GTID,position 也能用,只是要额外注意日志文件的完整性和顺序。
3. 备份基线怎么打,才敢在出事时真的拿来用
3.1 mysqldump 的关键参数,每个都有它的理由
数据量在几十 GB 以内,逻辑备份是最省事的选择。但随手一条mysqldump db > backup.sql是不够用的,下面这组参数我认为是标配:
mysqldump -h127.0.0.1 -P3306 -uroot -p \ --single-transaction \ --source-data=2 \ --flush-logs \ --routines --triggers --events \ --set-gtid-purged=OFF \ --default-character-set=utf8mb4 \ --databases order_db > /backup/order_db_$(date +%Y%m%d%H%M).sql逐个说清楚为什么。
--single-transaction让备份过程中的所有读操作处于同一个可重复读快照里,保证 InnoDB 表的数据是一致的。注意它只对事务型引擎有效,如果库里混着非事务引擎的表,一致性保证不了,这种情况必须用物理备份或者加锁。
--source-data=2(5.7 里叫--master-data=2)会把备份开始时的 binlog 坐标以注释形式写进 SQL 文件开头,这是重放起点的来源。设成 2 表示写成注释而不是直接执行CHANGE MASTER,避免恢复时误触发复制配置。
--flush-logs在备份开始前切一个新的 binlog 文件。好处是起点坐标正好落在新文件的开头附近,重放时截取范围清晰,不用在半个老文件里找位置。
--set-gtid-purged=OFF在开了 GTID 的环境里必须加。不加的话导出的文件里会带上SET @@GLOBAL.GTID_PURGED,恢复到新实例时会因为 GTID 集合冲突直接报错。恢复目标是独立实例时一般不需要带上原实例的 GTID 执行历史,关掉更省事。
3.2 数据量大时,逻辑备份会被物理备份替代
单库超过一两百 GB,mysqldump 的导出和导入时间会长到无法接受,恢复窗口根本压不住。这时候要换成基于数据页拷贝的物理热备工具,思路是直接复制 InnoDB 的数据文件,同时记录 redo 日志,最后通过一个 prepare 阶段把数据文件推进到一致状态。
物理备份的优势是快,几百 GB 的库恢复可能只要十几分钟;劣势是版本和平台相关,跨大版本恢复基本不可行,也不能只恢复单表(除非用导出单表的附加功能)。所以我的常见组合是:物理备份做日常基线,逻辑备份做单表级的补充和跨版本迁移。
不管用哪种,最终都要产出一份元数据文件,内容至少包括:备份开始和结束时间、binlog 文件名与起始 position、起始 GTID 集合、MySQL 版本、字符集、备份文件校验和。这份文件是出事时唯一能救命的说明书,比备份本身还容易被忽略。
| 备份方式 | 恢复速度 | 粒度 | 跨版本 | 适用规模 |
|---|---|---|---|---|
| 逻辑备份 | 慢 | 库/表级灵活 | 支持 | 几十 GB 以内 |
| 物理热备 | 快 | 实例级 | 基本不支持 | 几百 GB 以上 |
| 文件系统快照 | 最快 | 实例级 | 受限 | 有存储层支持时 |
3.3 备份完不做恢复演练,等于没有备份
我在实际工作中踩过最疼的一次坑,不是备份没做,而是备份做了三个月从没验证过。真出事的时候打开文件才发现,某个库的字符集参数写错了,中文全是乱码;更早一次是备份任务被 cron 里的超时杀掉,文件只有正常大小的三分之一,但退出码是 0,监控从来没报过警。
现在的做法很固定:每周从最近一次的备份里随机抽一份,在隔离实例上完整恢复一遍,检查三个指标——表数量对不对、几个关键表的行数量级对不对、随便挑几条业务数据内容对不对。同时记录整个恢复耗时,这个数字直接决定了真实故障时的 RTO。
4. 完整实操:把数据精准拉回误操作前的那一秒
4.1 先把时间线摊开
假设某公司的订单库,配置是 ROW 格式 + GTID 开启 + binlog 保留 7 天。事件顺序是这样的:
- 00:00 每日全量备份完成,元数据记录 binlog 起点为
mysql-bin.000123的 position 154,GTID 集合为3f2a...:1-880000 - 09:30:12 某开发在排查问题时执行了一条漏了 where 条件的 UPDATE,影响了订单表里的 12 万行
- 09:42 业务侧发现订单状态异常,报障
- 09:45 值班同学介入
此时最要紧的动作不是去查 SQL,而是先止住写入。原因很简单,每多写一秒,binlog 就多一段,误操作之后的有效数据也还在持续变化,越晚处理,需要丢弃或修正的数据越多。标准的做法是把应用侧切到只读,或者直接把业务库的连接摘掉,让写入停下来。
紧接着第二个动作:立刻把当前正在写的 binlog 从磁盘上拷出来,并执行一次FLUSH BINARY LOGS。之所以要拷贝,是因为后续可能会有人为了腾空间清理日志,或者实例重启导致日志轮转;拷到独立目录之后,这些日志就与生产实例解耦了,怎么操作都不怕。
-- 切一个新日志文件,把当前文件封口 FLUSH BINARY LOGS; SHOW BINARY LOGS;# 把相关 binlog 全部拷到备份盘 mkdir -p /backup/rescue_20240101 cp /var/lib/mysql/mysql-bin.00012* /backup/rescue_20240101/ ls -lh /backup/rescue_20240101/4.2 在 binlog 里把那个事务揪出来
接下来是定位。用mysqlbinlog把嫌疑时间段的日志解析成可读文本,关键在于两个参数:--base64-output=DECODE-ROWS把 ROW 格式的二进制事件解码成伪 SQL,-v输出详细的行变更信息。
mysqlbinlog --base64-output=DECODE-ROWS -v \ --start-datetime="2024-01-01 09:25:00" \ --stop-datetime="2024-01-01 09:35:00" \ /backup/rescue_20240101/mysql-bin.000123 > /tmp/inspect_0930.sql打开文件之后,关注几样东西:每个事务开头会打印BEGIN和 GTID(形如SET @@SESSION.GTID_NEXT='3f2a...:880123'),接着是### UPDATE order_db.orders加前后镜像的行数据,最后是COMMIT加# at 884512这样的位置标记。
判断哪个事务是元凶,看两点:受影响的行数和执行时刻。12 万行的更新在 ROW 格式里会有大量### UPDATE行,文件里肉眼可见地"特别长";时间戳也对得上 09:30:12。找到之后,把它前面那个位置记下来,这就是重放的终点。
如果开了 GTID,还有一个更快的办法:直接找那个事务的 GTID,后面重放时用--exclude-gtids把它排除掉,其余全部重放。这在"误操作之后还有正常业务写入、需要保留"的场景下特别有用。
注意:
mysqlbinlog的--start-datetime和--stop-datetime是按运行 mysqlbinlog 那台机器的本地时区去解析的,而 binlog 里存的是 UTC 时间戳。如果解析机和数据库不在同一时区,筛选出来的范围会整体偏移几个小时。我遇到过解析出来的日志"看起来一条都没有"的情况,最后发现就是时区差。
4.3 截断出可回放的片段
定位到误操作事务的起始位置之后,就要生成"从备份起点到误操作之前"这一段的重放脚本。这里分两种情况。
情况一,只恢复到误操作前,丢弃之后的所有变更。这是最常见的做法,命令里用--stop-position卡住终点:
mysqlbinlog --start-position=154 \ --stop-position=884500 \ /backup/rescue_20240101/mysql-bin.000123 \ | mysql -h127.0.0.1 -P3307 -uroot -p target_instance如果时间跨度跨了多个 binlog 文件,把文件名按顺序全部列出来,用空格或通配符连起来即可。mysqlbinlog会按给定顺序依次解析,中间不要漏文件,漏一个就会出现数据断档。
情况二,跳过误操作,保留之后的正常写入。这种就得用 GTID 排除:
mysqlbinlog --exclude-gtids='3f2a4c1e-...:880123' \ /backup/rescue_20240101/mysql-bin.000123 \ /backup/rescue_20240101/mysql-bin.000124 \ | mysql -h127.0.0.1 -P3307 -uroot -p target_instance这里有个细节值得强调:--stop-position的边界是不包含该位置的事件执行,也就是说停在那条事件之前。所以终点要取误操作事务BEGIN事件对应的位置,而不是它后面某个位置。取错了会导致误操作被部分重放,那比不恢复还麻烦。
4.4 在隔离实例上落地并校验
真正的恢复动作分三步走,全部在隔离实例上完成。
第一步,把全量备份导入。逻辑备份直接喂给 mysql 客户端:
mysql -h127.0.0.1 -P3307 -uroot -p < /backup/order_db_20240101000000.sql导入完成后立刻核对元数据里的坐标,确认这个备份的终点就是mysql-bin.000123:154。如果导入过程因为主键冲突、表已存在报错,说明目标实例不干净,必须清空重来,不要试图--force硬扛过去。
第二步,重放 binlog。用上一步准备好的片段,注意重放时最好临时关掉 binlog 记录,避免恢复过程自己又写一堆日志:
SET SESSION sql_log_bin = 0;第三步,校验。这一步最少要查三个层次:表数量与元数据是否一致、关键表的行数是否落在合理区间、抽样若干条业务记录逐字段比对。有条件的话,把核心表的CHECKSUM TABLE结果和备份时刻的记录做个对比,能更快发现问题。
4.5 切换与收尾
隔离实例验证通过之后,切换方式取决于业务架构。如果是主从结构,最稳的是把恢复好的实例提成新的主库,把应用连接切过去;如果只是单表数据出错,可以只把出问题的那张表导出来,在生产上做定点替换,代价小得多。
切换之后还有两件事别漏。一是把误操作的那条 SQL 和这次恢复的完整过程写成记录,包括定位到的 position、GTID、耗时、遇到的问题,这份记录下次能省一半时间。二是回到源头堵住漏洞:生产账号禁止直连、禁止不带 where 的批量更新、给变更操作加审批和 dry-run 环节。
5. 常见问题与排查技巧实录
5.1 时间对不上,八成是这几个原因
按时间筛选 binlog 却没筛到想要的事件,这是最高频的问题。我整理过几种成因。
时区问题,前面提过,解析机与数据库时区不一致。解决办法是用--start-position代替时间筛选,或者先把解析机时区调成和库一致。
长事务的时间戳错位。一个事务可能 09:00 开始,09:30 才提交,binlog 里 BEGIN 事件的时间戳是 09:00,COMMIT 是 09:30。如果按--start-datetime="09:25"去筛,这个事务的部分事件会被漏掉,导致解析出来的片段不完整。判断方法是看事务前后事件的时间戳跨度,跨度大的要特别小心。
从库上的 binlog 时间戳不是原始执行时间。从库应用 relay log 时,binlog 事件的时间戳取决于当时的TIMESTAMP处理方式,和主库执行时间可能有偏差。如果要在从库上做恢复,最好以主库的 binlog 为准。
5.2 binlog 缺失、损坏、被清空
这几种情况在事故现场并不罕见,处理思路各不相同。
过期清理是最常见的,PURGE BINARY LOGS或自动过期把需要的文件删了。这时候只能退回到最近一次全量备份,或者从从库、其他副本上找是否还有残留的日志文件。防范手段就是把保留窗口设够长,并且监控 binlog 文件的生成和清理情况。
主从切换导致文件编号重置。新主库的 binlog 从mysql-bin.000001重新开始,和旧主库的文件编号重叠,按文件名顺序重放会乱序。这时候必须靠 GTID 来定位,或者把文件重命名成带实例标识的格式。
文件损坏通常伴随磁盘异常或写入中断,表现为mysqlbinlog解析时报错退出。可以试试跳过损坏部分继续解析,但恢复出来的数据完整性就要打问号了,这种情况建议直接以最近的全量备份为准。
5.3 大事务、DDL、跨库操作这几个硬骨头
大事务没法在中间截断。MySQL 保证一个事务的所有事件写在同一个 binlog 文件里,所以即使max_binlog_size设成 1GB,一个 5GB 的事务也会让某个文件远超这个大小。这意味着你没法把重放停在事务内部,只能整个事务一起执行或者一起跳过。遇到这种大事务,重放时间长、锁持有久,最好在业务低峰做。
DDL 是隐式提交的,回滚不了。误执行DROP TABLE或TRUNCATE之后,binlog 里记录的是这条 DDL 本身,没有行级镜像可以反向解析。处理方式只有一条:恢复到 DDL 执行之前的时间点,然后单独把那张表导出来补回生产。所以线上环境的 DDL 权限要收得很紧,这个不是流程洁癖,是真金白银的教训。
跨库或多表事务在 ROW 格式下会按表分别记录事件,重放到只包含部分库的目标实例时会因为找不到表而报错。解决办法是重放时加上--rewrite-db做库名映射,或者在目标实例上预先建好空表结构。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查动作 | 处理方式 |
|---|---|---|---|
| 按时间筛不到事件 | 时区不一致 | 对比解析机与库的时区 | 改用 position 筛选 |
| 重放报主键冲突 | 起点选早了,重复重放 | 检查起点 position 是否等于备份终点 | 调整起点,或跳过冲突事务 |
| 重放报表不存在 | 目标库缺表或库名不同 | 核对表结构 | 先建表或用库名映射 |
| 解析日志报错退出 | 文件损坏或不完整 | 校验文件大小与校验和 | 从其他副本取日志 |
| 恢复后数据缺一段 | 中间漏了某个 binlog 文件 | 核对文件编号连续性 | 补齐文件重新重放 |
| 大事务重放卡住 | 事务涉及行数过多 | 看 ROW 事件数量 | 错峰执行,分批处理 |
| 时间点恢复后仍有重复数据 | 恢复前生产还在写 | 检查恢复期间是否有新写入 | 先冻结写入再恢复 |
5.5 一个很少人提但很好用的技巧
对于DELETE误删这类场景,如果只是少量行,其实不一定非得走全量恢复。ROW 格式的 binlog 里存了被删行的完整前镜像,理论上可以把它反向拼成INSERT语句,只把删掉的行补回去。有些开源脚本就是干这个的,也能自己写个解析程序处理。
但这个路子有几个前提:binlog_row_image必须是FULL,否则拼出来的行缺字段;目标表上不能有会冲突的唯一键;自增主键要显式指定值,不能让库自己生成。所以我的经验是,行数少、表结构简单时用反向解析补数据最快;行数多或者涉及多表关联时,老老实实走全量加 binlog 重放,别贪这个快。
6. 从"能恢复"到"敢恢复":把流程变成可重复的动作
6.1 备份策略分层,别用一套方案打天下
单一备份策略很难同时满足成本和恢复速度。我倾向的做法是分层:每周一次全量物理备份做基线,用于大范围恢复;每天一次逻辑备份,用于单表级别的定点修复和跨环境迁移;binlog 持续归档到独立存储,覆盖窗口按业务重要性定,核心库至少 7 天,重要库 3 到 7 天。
归档这块要特别注意:binlog 不能只留在数据库所在的那块盘上。数据库盘出问题的时候,数据和日志一起没,恢复链路直接断掉。定期把 binlog 同步到独立的备份存储,并且校验传输完整性,这是把"能恢复"变成"一定能在灾难后恢复"的关键一步。
6.2 把恢复演练做成例行任务
恢复能力不是文档写出来的,是练出来的。我现在习惯把演练做成固定周期的事:每月挑一个非核心库,模拟一次误删,从发起故障到数据校验完成,全流程走一遍,记录每一步的耗时和卡点。
这个过程中最有价值的信息往往不是"花了多久",而是在哪个环节卡住了。可能是找不到备份元数据文件,可能是隔离实例版本和备份版本不匹配,可能是某个库的字符集参数不对。这些细节在真出事时会被无限放大,提前踩一遍就都不是事了。
6.3 几条我认为必须写进规范的事
备份必须带元数据文件,位置、GTID、时间、版本、校验和,一样不能少,并且和数据文件放在一起。
恢复必须在隔离环境完成验证后再切换,不允许直接在生产库上试。
生产账号禁止直连执行 DML,尤其是批量更新和删除,走变更平台加 dry-run。
binlog 归档独立存储,且保留窗口要覆盖"最晚可能发现误操作"的时间,不是平均值。
每次恢复都留一份复盘记录,记录误操作 SQL、定位坐标、恢复耗时、踩到的坑,形成团队自己的知识库。
这几条听起来都很朴素,但真出事的时候,能同时做到这五条的团队,恢复过程往往波澜不惊;做不到的,基本都是在现场重新发明流程。
我个人在实际操作中的体会是,时间点恢复这件事,技术门槛其实不高,命令一共就那么几条,难的是在出事的那一刻,所有前置条件都已经就位。binlog 开着、保留窗口够长、备份带坐标、隔离实例随时能起、恢复演练做过几遍——这五件事里有任何一件没做,现场就会多花几个小时去补救。所以与其在事故中研究怎么恢复,不如平时把这几件事按周按月地做扎实,出事时打开文档照着敲命令就行。真正让人安心的从来不是"我知道怎么恢复",而是"我上个月刚恢复过一次,用了 47 分钟"。