十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL误删数据恢复:binlog反解补偿SQL与四条路线

MySQL误删数据恢复:binlog反解补偿SQL与四条路线 1. 误删发生后的头十分钟先别急着敲恢复命令凌晨两点半接到电话对面声音是抖的我把生产库的表清了。这种场面我遇到过不止一次MySQL 误删数据恢复这件事最怕的不是数据没了而是人慌。很多人第一反应是打开搜索引擎找工具然后一边手抖一边在生产库上敲命令——实话讲这么干十次有九次会把局面弄得更糟。真正决定恢复成败的往往不是后来用了多高级的工具而是最初那十分钟你有没有管住自己的手。我先把一个反直觉的结论摆在这儿误删之后的第一个动作不应该是恢复而是冻结现场。你需要让这台机器上的写入尽可能停下来让 binlog 不再被新事务往前推让被删表的磁盘页尽可能不被覆盖。数据恢复本质上是在和时间以及磁盘写入抢东西抢得越早能捞回来的比例越高。这套流程适合所有需要自己扛线上库的开发和运维哪怕你只是在一台测试机上练手也建议照着走一遍因为流程本身比工具更值钱。1.1 分清三种删DELETE、TRUNCATE、DROP 的恢复天花板完全不同同样是数据没了底层的杀伤力差着量级先分清类型再决定投入多少资源。第一种误执行了 DELETE。比如DELETE FROM orders;忘了写 WHERE或者 WHERE 条件写错。这种最温柔因为 InnoDB 执行 DELETE 做的是标记删除——行记录被打了删除标记事务提交后由后台 purge 线程异步真正清理。只要 purge 还没扫到理论上数据页里还留着残影。更重要的是ROW 格式的 binlog 会把每一行删除前的完整字段值记下来这是最理想的恢复素材。第二种TRUNCATE。这条语句在 MySQL 里的语义是删表重建——它不走逐行删除而是新建一个空的表空间再把老的丢掉速度极快但代价是 binlog 里通常只留一条 DDL 语句没有行级数据。所以 TRUNCATE 的恢复基本只能依赖备份 binlog 前滚或者从其他副本/页扫描硬捞。第三种DROP TABLE。最狠表定义和 ibd 文件一起被删掉。恢复不光要捞回数据还得先找回建表语句——这点后面会重点讲因为很多工具的坑就埋在这里。提示这三种操作在 binlog 里的表现形式完全不同判断类型不要去问执行的人直接去翻 binlog谁执行了什么、几点几分一条不落。1.2 为什么止血比抢救更重要InnoDB 的删除是标记删除被标记的行不会立刻从磁盘页里消失但会被后续的写入慢慢覆盖。binlog 是一个追加写的文件流新的事务一直在往里写如果你要恢复的时间点比较久远很可能老的 binlog 已经因为expire_logs_days8.0 是binlog_expire_logs_seconds到期被自动清理掉了。这两个机制共同决定了一件事每多写一秒可恢复的概率就掉一点。所以止血动作要按顺序来立刻通知业务方如果是写多读少的业务先把写入入口切走或者降级只读查询可以保留读不会破坏现场。在从库上确认从库是否已经同步了这条误删。如果从库还没执行到先STOP SLAVE8.0 是STOP REPLICA把它钉住这就是一个天然的误删前快照。如果开了延迟从库立刻把延迟复制停掉别让它继续追。检查SHOW VARIABLES LIKE log_bin、SHOW MASTER STATUS把当前的 binlog 文件名和 position 记下来。在恢复完成之前禁止任何人在这台实例上做FLUSH LOGS、RESET MASTER、重建表之类的操作。第 5 条听起来是常识但我见过真实的翻车有同学为了减少干扰手贱执行了FLUSH LOGS切了新 binlog结果做区间定位的时候把文件对应关系弄错了白白多花了两个小时。1.3 动手前必须搞清楚的六件事我习惯在开始恢复之前先填一张信息登记表六项内容缺一项就别往下走序号需要确认的信息为什么重要1误删的准确时间点精确到秒含时区决定 binlog 定位区间差一分钟可能多回放几万行2误删的语句原文判断是 DELETE / TRUNCATE / DROP决定走哪条路线3binlog_format和binlog_row_image决定能不能拿到完整的行数据MINIMAL 会缺字段4最近一次可用全量备份的时间与位置决定前滚的起点备份越新回放代价越小5是否存在延迟从库或未追上的从库存在的话这是成本最低的路线6被删表的结构列顺序、类型、字符集反向生成 INSERT 时必须严格对齐列顺序第 3 项里有个细节值得单独说binlog_row_image有三个取值FULL记录变更前后的所有列MINIMAL只记录被改动的列DELETE 时只记录主键或唯一键NOBLOB是不记录未变更的 BLOB/TEXT。只有FULL才能保证你拿到整行的原始值。我在 1.1 里说 ROW 格式会把每一行删除前的完整字段值记下来严格讲这句话的前提就是binlog_row_imageFULL——这个前提不成立的话后面所有基于 binlog 的反向恢复方案都要打折这是很多人第一次做恢复时最容易忽略的地方。第 6 项也容易被轻视。REDO 和 binlog 都不会存列名Table_map 事件里只有表 ID 和列数量### 1... 2...这种形式出现的只有列的位置编号。你拿到的是第 1 列等于 1001第 2 列等于 张三但你要变成INSERT INTO orders (id, name, ...) VALUES (1001, 张三, ...)就必须要有一份可靠的建表语句。表还在的话SHOW CREATE TABLE拿一下就行如果表已经被 DROP那就得去备份里、去代码仓库的 migration 文件里、去 DDL 审计平台里找这一步找不齐后面全白搭。2. 四条恢复路线的选择逻辑与代价对比现场信息登记完之后你面对的其实是四选一或者组合的问题。选路线的核心原则只有一条从代价最低、风险最小的方案往上试不要一上来就抡大锤。下面四条路线按推荐顺序排列能走第一条就别走第四条。2.1 路线一延迟从库或未受影响的从库直接顶上如果有延迟从库延迟时间设为 1 到 2 小时那这条路线几乎是无痛的。做法是# 在延迟从库上停止复制找到误删前的 GTID 或 position mysql -h delayed-replica -uroot -p -e STOP REPLICA; SHOW REPLICA STATUS\G停住之后这个从库上的数据就停在误删之前。你可以直接把需要的表用mysqldump导出来或者只把缺失的那部分数据导回主库。如果从库还没追上误删那条事务同理先停复制。这条路线的好处是快、准、几乎零风险坏处是依赖你提前做了延迟从库。很多人觉得延迟从库占机器不值等真出事的时候才知道那点机器钱花得太值了。2.2 路线二全量备份 binlog 前滚到误删前一秒这是最正统的恢复方式也是真正意义上的完整恢复。思路是先还原最近一次全量备份到一个全新的临时实例注意绝对不要在出问题的生产实例上直接还原然后从备份对应的 binlog position 开始一直回放到误删语句之前的那一条事务把误删及其之后的事务全部丢掉。这套流程的关键点是起点怎么定。全量备份如果用 mysqldump 加--master-data2或者--source-data2会把备份时刻的 binlog 文件和 position 写进 SQL 文件头部的注释里搜一下CHANGE MASTER TO就能找到。XtraBackup 之类物理备份则会在xtrabackup_binlog_info文件里记录同样的信息。找到这个起点恢复就有了确定性的锚。回放的时候我一般用--stop-position停在误删事务的前一个position 上而不是用时间点。因为--stop-datetime是秒级精度同一秒里可能有好几条事务用时间点很容易把误删语句本身也带进去。2.3 路线三没有可用备份靠 binlog 反向生成补偿 SQL如果全量备份太老比如一周前回放的代价太高或者压根就没有备份那就换成反向补偿的思路不还原整库只把被删的那些行从 binlog 里抠出来重新插回去。具体做法是解析 binlog找到误删事务的 DELETE 事件把它翻译成等价的 INSERT 语句然后在生产库或者先在一个临时库验证过之后执行。这套方案的好处是只补齐缺失的数据不影响其他任何东西代价小、可控性强。坏处是它对 binlog 的要求高必须是 ROW 格式、binlog_row_imageFULL、binlog 文件还在保留期内。详细操作我在第 3 章展开。2.4 路线四binlog 也断了页级扫描硬捞 ibd这是最后的兜底。当你既没有备份binlog 也被覆盖或者压根没开剩下的选择就只有从 InnoDB 的物理文件里硬扫了。核心原理是InnoDB 的记录在数据页里是明文未压缩未加密的情况下被标记删除的行在 purge 之前仍然存在于页中可以用工具按页扫描把残影捞出来。这条路线能捞回多少取决于三件事purge 线程跑了多久、磁盘上的页被覆写了多少、表的行格式有多规整。我在第 4 章会讲具体用什么手段以及一个非常好用的土办法——直接在 ibd 文件里做字符串搜索。2.5 一张表把四条路线说清楚路线前置条件恢复完整度风险大致耗时延迟从库 / 未同步从库有延迟从库或从库未追上完整低分钟级全量备份 binlog 前滚有可用备份 完整 binlog完整中需临时实例小时级binlog 反向补偿ROW FULL binlog 在保留期内与被删行等量中低小时级页级扫描硬捞只有 ibd 或磁盘残留部分可能缺行高数小时到数天这张表我在几次恢复里都临时画过它的最大价值不是告诉你选哪条而是让你在压力最大的时候还能有条理地往下走而不是凭感觉乱试。还有一个组合技值得记住路线二和路线三可以混用——用老备份还原出历史数据再用 binlog 的反向 SQL 把最近的增量补齐两边时间轴不会重叠这是很多老手实际的做法。3. 用 binlog 反解出补偿 SQL 的完整链路这一章是整篇的核心。我把它拆成一条从确认格式到最终验证的完整链路每一步都给出可执行的命令和判断依据。3.1 先确认 binlog 格式ROW FULL 才有完整还原能力别凭印象直接查SHOW VARIABLES LIKE binlog_format; SHOW VARIABLES LIKE binlog_row_image; SHOW VARIABLES LIKE binlog_rows_query_log_events;期望结果是binlog_formatROW、binlog_row_imageFULL。如果是STATEMENT格式binlog 里存的是 SQL 原文DELETE FROM orders WHERE create_time 2024-01-01那你能看到的就只有这句话本身拿不到被删的行值。这时候反向补偿这条路走不通只能回到路线二或者从其他副本想办法。如果是MIXED就要看具体那条语句当时走的是哪种格式不确定性很高得实际打开 binlog 看。如果binlog_row_imageMINIMALDELETE 事件里只会记录主键值。你确实能知道哪几行被删了但拿不到其他字段补回去的行会是一堆 NULL 或者默认值。这种场景下我能想到的补救办法是先用主键定位再去其他表或者日志里反查字段值工作量大且不一定做得完。顺便说一下binlog_rows_query_log_events打开之后 ROW 格式的 binlog 里会额外附带原始 SQL 文本作为 Rows_query 事件排查的时候一眼就能看出是哪条语句干的非常省事。生产环境我一般都建议开。3.2 用 mysqlbinlog 把事件流翻译成人能看的文本mysqlbinlog是官方自带的解析工具不需要额外安装。ROW 格式的 binlog 默认是 base64 编码的二进制直接 cat 出来没法看必须加-vv让它解码成可读形式mysqlbinlog \ --base64-outputdecode-rows \ -vv \ --start-datetime2024-05-20 02:00:00 \ --stop-datetime2024-05-20 02:20:00 \ /var/lib/mysql/mysql-bin.000123 \ /tmp/parse_win.txt几个参数的作用和坑--base64-outputdecode-rows把 BINLOG 语句换成###开头的注释行这是可读输出的关键。忘了加这个你看到的就是一大坨 base64。-vv会额外打印列的类型元信息比如/* INT meta0 nullable0 is_null0 */。这个信息在判断列类型和是否可空时非常有用。--start-datetime/--stop-datetime用的时区是服务器的本地时区不是 UTC也不是你终端的时区。这条我踩过坑后面第 5 章细讲。输出重定向到文件一定要做。生产库的 binlog 动辄几个 G直接打到终端上会把你的 SSH 会话拖死。解析出来的内容长这样### DELETE FROM shop.orders ### WHERE ### 11001 /* INT meta0 nullable0 is_null0 */ ### 22024-05-20 01:58:31 /* DATETIME meta0 nullable1 is_null0 */ ### 3299.00 /* DECIMAL(10,2) meta0 nullable1 is_null0 */ ### 4张三 /* VARSTRING(60) meta60 nullable1 is_null0 */看到### DELETE FROM这一组就说明找到了要反向处理的事务。注意它前面还会有BEGIN、Table_map事件后面有COMMIT或者XID定位区间的时候要以事务为单位不能只截 DELETE 那几行。3.3 圈定误删区间position 和 GTID 两种定位方式先要找到误删事务的准确边界。打开上一节生成的parse_win.txt在文件里搜DELETE FROM往上找到最近的BEGIN往下找到最近的COMMIT或XIDMySQL 8.0 用 XID 事件中间这一整段就是事故事务记下它的end_log_pos。如果实例开了 GTIDmysqlbinlog输出里会看到SET SESSION.GTID_NEXTxxxxxxxx-...:12345这个 GTID 就是事故事务的唯一标识比 position 更好用因为它跨文件有效。有些场景下误删不是单条语句而是一批语句连续执行比如脚本循环删了十来张表。这种情况我更推荐用--start-position和--stop-position圈一个区间然后人工在输出里逐条核对而不是用时间点。用时间点的问题是很容易漏掉边界条件的判断——你以为那 5 秒里只有一条误删语句实际上还有正常的业务写入混在里面一刀切下去会把正常数据也丢掉。3.4 把事件翻成反向 SQL删变插、插变删这一步可以纯手工也可以用工具。手工的思路是打开解析结果把### DELETE FROM db.t和后面的### WHERE ### 1... 2...抄下来改成INSERT INTO shop.orders (id, create_time, amount, name) VALUES (1001, 2024-05-20 01:58:31, 299.00, 张三);行数少的话手工没问题几十行以内行数多就必须脚本化了。写脚本有三个关键点第一列顺序必须严格对齐。1到N对应的是表定义里的列顺序包括那些你没在 SELECT 里注意过的列。列名要单独从SHOW CREATE TABLE或备份里的 DDL 拿。如果表已经被 DROP那就得从代码仓库的 migration 文件、DDL 审计记录、甚至其他环境测试库、预发库里找同一张表的建表语句——这一步是最容易卡住的地方一定要提前准备。第二值的转义要正确。字符串里的单引号、反斜杠、换行符都要转义BLOB 和二进制列要按十六进制处理0x...。这个细节不处理好插回去的数据可能是错的而且还不会报错特别阴险。第三执行顺序要注意外键和唯一键。如果表上有唯一索引恢复之前要确认那些主键没被复用有外键依赖的话按依赖顺序插。我的习惯是先SET FOREIGN_KEY_CHECKS0导完再打开同时用INSERT ... ON DUPLICATE KEY UPDATE或者INSERT IGNORE兜底避免因为一条重复导致整批失败。写一个 Python 脚本骨架大概是这样逻辑就是从 mysqlbinlog 的输出里按事务块切分、按列序号对齐import re COLS [id, create_time, amount, name] # 必须和表定义一致 def parse_delete_block(lines, table): 把 mysqlbinlog -vv 输出里的 DELETE 块转成 INSERT 语句 # 每行形如: ### 11001 /* INT meta0 nullable0 is_null0 */ pat re.compile(r###\s(\d)(.*?)\s*/\*) values {} for ln in lines: m pat.search(ln) if not m: continue idx, raw int(m.group(1)), m.group(2).strip() # 去掉可能的 NULL 标记 if raw.upper() NULL: values[idx] NULL elif raw.startswith() and raw.endswith(): escaped raw[1:-1].replace(\\, \\\\).replace(, \\) values[idx] f{escaped} else: values[idx] raw ordered [values.get(i 1, NULL) for i in range(len(COLS))] col_sql , .join(f{c} for c in COLS) return fINSERT INTO {table} ({col_sql}) VALUES ({, .join(ordered)});这个脚本只是骨架实际用的时候要处理BEGIN/COMMIT分块、多表混排、事务边界这些情况。如果不想自己写社区里有几个成熟的解析工具思路都是类似的选一个维护活跃的就行但务必记牢这类工具只会帮你生成 SQL不会替你做验证。3.5 在临时实例上做全量回放验证生成好的补偿 SQL第一遍绝对不要在生产库执行。正确姿势是用一台临时实例或者本地 Docker 起一个同版本的 MySQL把表结构导过去。把生成的 SQL 全部导进去观察有没有语法错误、主键冲突、字符集告警。比对行数和关键字段的分布SELECT COUNT(*)、SELECT MIN(id), MAX(id)、以及几个业务关键字段的抽样值。确认无误后再对生产库执行。这里有一个非常值得花时间的动作先在生产库上做一次只统计不写入的核对。比如用生成的数据构造一个临时表和线上的表做LEFT JOIN找出差异行确认不会撞主键。这个动作多花二十分钟能省掉一次二次事故。4. 没有备份也没有完整 binlog 时的兜底手段前面三章讲的是有牌可打的情况。现实中还有一种很难受的局面binlog 没开或者早就过期被清了备份也只有几个月前的一份。这时候就只能从物理层和周边系统里想办法了。4.1 从 ibd 文件里做页级扫描先讲原理。InnoDB 的表空间文件.ibd由一个个 16KB 的页组成页头里有个FIL_PAGE_TYPE字段标识这个页的类型0x45BF17855表示这是 B 树的索引页行数据就存在索引页里。DELETE 只是给行记录打上删除标记在页面里按记录链表的方式跳过去数据本身还在。需要注意的是DROP TABLE会直接把 ibd 文件从文件系统删掉所以这条路线对 DROP 场景的适用性取决于你能不能从文件系统的层面找回被删的文件或者有没有做快照、有没有其他机器上还留着同一个表的物理文件。实际情况里更常见的路径是从其他环境里找到一份 ibd 的副本比如从库、测试环境、备份介质上捞出来然后做页扫描。页级扫描的做法专业工具是一类但我想推荐一个特别土却极其有效的办法直接在 ibd 文件里做字符串搜索。因为行数据在页里是明文存储的前提是表没压缩、没加密你只要grep或者用十六进制编辑器搜一个你记得的特征字段比如手机号、订单号、身份证后缀就能定位到那一行数据在文件里的位置然后手动把附近的字段拼出来# 找一个已知的特征值看它在 ibd 文件里出现了几次 grep -a -c 1380000 /backup/orders.ibd # 导出附近上下文手工分析字段边界 grep -a -b -o .\{0,120\}1380000.\{0,120\} /backup/orders.ibd | head -50这个方法不优雅但在几百万行里找回几百条关键记录的时候效率比写完整解析器高得多。记住两个前提表没有用压缩行格式COMPRESSED也没有开表空间加密否则数据是没法直接搜到的。4.2 从 general log、慢日志、业务日志、消息队列里钓回数据这条路线经常被忽视但实际命中率不低。思路是数据虽然在 MySQL 里没了但在别的地方可能还留着痕迹。general log如果开了general_log所有到达 MySQL 的语句都会被完整记录包括那些 INSERT 的原始值。翻出误删时间点之前的那段时间把相关 INSERT 捞出来重新执行一遍这是最直接的一条路。缺点是 general log 体积巨大、通常不会长期开启而且记录里可能包含不需要的信息恢复时要注意筛选。慢查询日志只记录执行时间超过long_query_time的语句命中率低但如果误删的表有大批量导入的记录可能会留下线索。应用层日志很多业务在下单、支付这些关键环节会打日志日志里往往带着完整的表单数据。这是最靠谱的一条外部线索。消息队列如果业务走的是事件驱动架构创建/更新的消息可能还在 MQ 里或落到了数据同步表重新消费一遍就能把数据补回来。ES 或数据仓库如果平时有把 MySQL 数据同步到搜索引擎或者数仓那边的数据是独立的副本直接查出来回写即可。这条经常是最快的。我给一个实操建议平时就把每条核心业务数据的另一个副本在哪写进应急预案。这句话听起来像官话但我确实见过有团队因为提前把订单数据同步了一份到数仓误删之后二十分钟就把数据补齐了而隔壁团队花了三天。4.3 恢复之后怎么确认数据真的齐了不管走哪条路线恢复完不校验等于没恢复。校验的层次我一般分三层第一层是行数校验。用SELECT COUNT(*)对比恢复前后的预期量最好带上时间范围条件比如WHERE create_time BETWEEN ... AND ...因为整表行数可能会因为正常业务写入而变化。第二层是主键与边界校验。检查被恢复区间的主键是否有空洞、是否有重复SELECT COUNT(*)和SELECT COUNT(DISTINCT id)是否相等。这一层能抓出丢行和重复插入两类问题。第三层是业务字段抽样校验。挑十行左右把每个字段的值和你能找到的原始线索应用日志、MQ 消息、截图、导出文件逐一对照。这一层最花时间但它是唯一能发现值错了但结构对了这类隐蔽问题的手段比如字符集转换错误、时间戳偏移、金额精度丢失。5. 我在几次真实恢复里踩过的坑前面讲的是方法论这一章讲我在实际操作里真真切切翻过的车。坑这种东西别人讲了你不一定记住自己踩一次就忘不了所以能提前知道就尽量提前知道。5.1 坑一binlog_row_imageMINIMAL恢复出来的行缺字段第一次遇到这个坑的时候我信心满满地写好脚本把 DELETE 事件全转成了 INSERT跑完之后一查行数是回来了但除了主键其他字段全是 NULL。查了半天才想起来看binlog_row_image结果是MINIMAL。这个参数为什么会被设成 MINIMAL因为它是为复制优化设计的——只记录变更的列能减少 binlog 体积主从复制仍然能正确应用。问题在于它的设计目标里根本不包含给人类做数据恢复用这就是典型的参数为某个场景优化却坑了另一个场景。验证方法很简单随便找一个 DELETE 事件看### N是不是只有一两行主键而不是全部列。如果只看到主键那就别指望反向补偿了换路线。补救的办法只有从其他副本或者外部数据源反查字段值工作量大得离谱所以这是个必须在事前就防住的坑。5.2 坑二字符集和时区不一致中文变问号、时间偏移 8 小时有一次恢复出来的中文全是???。原因是我生成 SQL 的时候用了默认的latin1连接而原始数据是utf8mb4。生成的时候没报错导入的时候也没报错数据安安静静地就坏了。正确做法是生成文件的编码、mysql客户端的--default-character-set、连接串里的字符集、表的字符集四处必须一致统一用utf8mb4。恢复脚本文件保存的时候也要明确标注编码别用系统的默认值。时间偏移的问题更隐蔽。mysqlbinlog --start-datetime和--stop-datetime用的是服务器本地时区如果服务器是 UTC你的终端是东八区你按本地时间传进去区间会整体偏移 8 小时抓到的根本不是你要的那段反而可能把误删语句本身包进去。我的做法是一律用 position 或 GTID 定位不碰时间参数如果非要用时间区间比如不知道 position 的时候先查SELECT global.time_zone, session.time_zone, NOW()把时区对齐再用SELECT CONVERT_TZ()换算清楚。5.3 坑三GTID 冲突与 --skip-gtids 的副作用从备份还原到临时实例的时候如果打开了 GTID还原进去的事务带着原来的 GTID很容易和实例上已有的 GTID 撞车报GTID_PURGED冲突或者ERROR 1840。网上的标准解法是导的时候加--skip-gtids让新实例自己重新生成 GTID。这个解法本身没错但它有个副作用GTID 被重新生成之后你生成的那批反向 SQL 执行到生产库上时会和主库已有的 GTID 集合产生不可预期的关系特别是当你要用mysqlbinlog输出直接管道给 mysql 执行的时候很容易出现执行了一半报了 GTID 冲突然后停住的情况。我的处理方式是补偿 SQL 不走 GTID 通道用SET SQL_LOG_BIN0或者干脆关掉这一批会话的 binlog 写入把反向 SQL 当普通 SQL 执行。同时注意SET SQL_LOG_BIN0只影响当前会话且需要足够权限。执行完之后再确认主库的 GTID 执行集合没有异常。5.4 坑四在生产库上直接回放二次伤害这个坑听起来最蠢但它是我见过发生频率最高的。有人图省事直接在生产库上执行mysqlbinlog ... | mysql -uroot -p结果因为定位区间多算了把误删之后新产生的正常数据也一起回放了——比如一条UPDATE事件被重新执行把客户刚改的地址又改回旧值。铁律先在临时实例上完整跑一遍比对结果再动生产。临时实例的搭建成本很低现在用容器几分钟就能起一个同版本的 MySQL。另外即使是在临时实例上跑也建议先用mysqlbinlog --stop-position精确定位跑完SELECT COUNT(*)和几个关键值核对确认无损再复制到生产执行。5.5 坑五mysqlbinlog 解析大 binlog 把磁盘打满这个坑比较技术性但很常见。一个 8GB 的 binlog 用mysqlbinlog -vv解析出来纯文本可能有 40 到 60GB因为每一行都会展开成多行的###注释。有同学直接把输出导到/tmp结果/tmp撑爆导致 MySQL 或者其他服务异常。预防方法输出前先df -h看一下目标分区的剩余空间按 binlog 大小的 8 到 10 倍预留。用--start-position和--stop-position把区间缩小到事故发生的那几分钟别整个文件解。需要长时间处理的话直接用管道流式处理不要落盘mysqlbinlog --base64-outputdecode-rows -vv \ --start-position1234567 --stop-position1234890 \ /var/lib/mysql/mysql-bin.000123 \ | python3 gen_recover_sql.py \ /backup/recover_orders.sql这样磁盘上只留最终生成的那份 SQL中间过程全部在内存里流转。6. 让恢复这件事永远用不上把防线往前挪把恢复流程背得再熟也不如让误删根本不发生。这一章讲几个成本很低但收益很高的动作都是我在线上环境里长期用下来觉得值得的。6.1 权限与入口把 DELETE 的权限收进盒子第一个动作是收缩高危权限的分配。生产库的写账号里只有极少数通常是应用连接账号才需要DELETE权限人类的运维和开发账号默认不应该有。日常需要清理数据的时候走审批 存储过程或者专门的清理平台而不是让人手动敲语句。第二个动作是打开 sql_safe_updates。这个参数打开之后UPDATE和DELETE语句必须带WHERE条件而且WHERE的条件里必须用到索引列否则直接报错拒绝执行。这个机制的价值在于它拦的是忘记写 WHERE这个最高频的误删原因。设置方式SET GLOBAL sql_safe_updates ON; -- 会话级别同样生效需要足够权限 SET SESSION sql_safe_updates ON;要注意的是打开之后有些批量更新的脚本会因为条件不走索引而被拒绝需要提前和业务方对齐。这本身就是个好事——它顺便帮你发现了一批全表扫描的慢 SQL。第三个动作是统一的语句审计。所有 DDL 和高危 DML 走工单系统执行前后自动留档。真出事的时候这套记录能帮你省掉大量排查时间。6.2 备份策略与 binlog 保留时长的计算备份不是有就行关键指标是 RTO恢复需要多久和 RPO能容忍丢多少数据。以我们线上的一套订单库为例我的配置是这样的每天凌晨一次全量物理备份速度快、恢复快。每小时一次增量保留 72 小时。binlog_expire_logs_seconds设为6048007 天。binlog 保留时长怎么定我的算法是从最长可能多久之后才发现问题倒推。举几个实际情况场景发现问题的时间需要 binlog 保留多久线上监控告警秒级发现分钟级24 小时足够业务方次日对账才发现次日上午至少 3 天月底财务对账才发现一个月30 天成本很高通常靠备份兜保留时间越长磁盘占用越大。一个经验值日均写入量大的库binlog 每天可能产生几十 GB7 天就是几百 GB。所以这个参数要和磁盘容量一起算别拍脑袋填个 30 天然后把盘撑爆。另外expire_logs_days在 8.0 里已经废弃被binlog_expire_logs_seconds取代了如果你的配置里还有旧参数记得换掉。6.3 延迟从库的延迟时长怎么定延迟从库的核心参数是在CHANGE REPLICATION SOURCE TO里的SOURCE_DELAY旧语法是MASTER_DELAYCHANGE REPLICATION SOURCE TO SOURCE_HOSTprimary-host, SOURCE_USERrepl, SOURCE_DELAY3600, SOURCE_AUTO_POSITION1;3600 秒就是一小时的延迟。这个值定多少合适我的判断依据是**误删到发现之间的典型时间窗**。如果团队能在五分钟内发现并响应那延迟 15 到 30 分钟就够了如果经常是业务方隔几小时才反馈那就得设到 2 小时以上。设得越长从库的数据越旧日常用它做读分流的价值越低所以通常会把延迟从库单独放在一台机器上专用于兜底不承担线上读流量。6.4 把恢复流程写成 runbook 并定期演练最后这条我认为是收益率最高的把上面所有流程写成一个可执行的 runbook然后每季度真的演练一次。runbook 里应该包含故障判定标准、止血动作清单、四条恢复路线的决策条件、每条路线对应的具体命令和脚本位置、临时实例的搭建方式、数据校验清单、以及事后复盘模板。写的时候用命令行的形式别写成散文因为出事的时候没人有心情读散文。至于演练我一般会挑一个非核心的表在预发环境里真的删掉然后让值班的同学按 runbook 走一遍掐表记录每一步的耗时。第一次演练经常会暴露出各种问题备份恢复脚本里的路径写死了、临时实例的 MySQL 版本和线上不一致导致行格式兼容性问题、校验脚本缺依赖、或者干脆是拿不到那台机器的权限。这些问题在演练中暴露出来的成本很低在生产故障现场暴露出来的成本就完全不一样了。我个人的体会是这套东西的价值不在于恢复得多快而在于心里有底。做了延迟从库、把 binlog 保留时间算清楚、把反向恢复脚本提前写好并在演练里跑通过那么哪怕真的有人手滑删了表你也只是在执行一个已经被验证过三遍的流程而不是在凌晨三点半凭感觉临场发明一套方案。这两种状态下做出的决定质量差得不是一星半点。
返回列表