ARTICLE DETAIL

资讯详情

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

MySQL误更新后怎么救?从止损到binlog闪回与PITR的完整恢复指南

MySQL误更新后怎么救?从止损到binlog闪回与PITR的完整恢复指南 凌晨一点接到朋友电话声音都在抖他把线上订单表的支付状态 update 错了几十万行状态全变。这种场景我处理过不止一次说实话MySQL 数据误删或者误更新之后能不能救回来七成取决于你动手恢复前的 20 分钟做了什么而不是你用哪个工具跑得多快。这篇就把我自己的完整恢复流程写出来——怎么止损、怎么确认底牌、怎么用 binlog 闪回、怎么备份时间点恢复连没有 binlog 时的文件级玩法也写上每一步都到命令级别照着做就行。1. 误操作后的黄金20分钟先止损再想恢复1.1 停掉所有写入方法比想象中粗暴先把业务写入口全部切断。别急着去翻文档、跑恢复工具这时候每多一笔新写入都可能把旧的数据页覆盖掉尤其是后面要走的文件级恢复写入等于在焚尸。实际操作就三步停应用服务或者至少停掉所有定时任务、消息队列消费者。这是最干净的方式。如果应用停不下来比如线上流量还在直接在 MySQL 里开启全局只读SET GLOBAL read_only ON; SET GLOBAL super_read_only ON;注意read_only只拦普通账号拦不住 SUPER 权限的账号所以super_read_only也要一起开双保险。这一步做完新连接里的写语句会被直接拒绝。把已经挂着的写连接清掉SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist WHERE command Sleep;看哪些连接正在执行 INSERT/UPDATE/DELETEKILL id;掉。还有一类场景容易被忽略如果你的库是从库光开只读没用主库的复制线程还在源源不断把新变更同步过来。这时候要停复制线程。MySQL 8.0.22 以前是STOP SLAVE;之后是STOP REPLICA;注意版本。我见过有人误删后第一反应是FLUSH LOGS把 binlog 切到新文件觉得这样好定位。这种操作在止损阶段完全多余还可能让你后面恢复时少一个文件千万别做。1.2 快速确认边界哪些东西还在哪些已经没了在动手恢复之前你要在十分钟内回答这几个问题误操作类型是什么UPDATE、DELETE、还是 DROP/TRUNCATE影响哪张库表大概影响多少行有没有全量备份备份点是什么时候binlog 开了没有格式是什么保留多少天这些问题的答案决定了你走哪条恢复路线。DML 误操作UPDATE/DELETE可以针对性闪回DDL 误操作DROP/TRUNCATE基本只能靠备份binlog 时间点恢复PITR。确认当前 binlog 状态用这几条命令SHOW VARIABLES LIKE log_bin; SHOW VARIABLES LIKE binlog_format; SHOW VARIABLES LIKE binlog_row_image; SHOW BINARY LOGS;SHOW BINARY LOGS能列出当前所有 binlog 文件配合SHOW MASTER STATUSMySQL 8.4 以后建议用SHOW BINARY LOG STATUS可以看到当前写入位点方便后面确认日志有没有被覆盖过。1.3 别急着重启别急着重建表误操作之后除非你确认是 MySQL 本身 crash 了否则不要重启实例。重启本身不会删 binlog但如果你后面要走文件级恢复重启会让被 rm 掉的文件句柄彻底失效唯一的机会直接断送。另外绝对不要 为了恢复个干净环境 去 DROP 原表再 CREATE 一张空表。听起来像重置现场实际上是在毁掉恢复线索。你需要的是一台全新的临时实例来做恢复实验原库保持原样什么都不动。止损阶段做得越干净后面恢复的成功率越高。我处理过的案例里凡是当时冷静下来先停写、再确认状态的基本都找回来了凡是手忙脚乱又执行了一堆幺蛾子命令的最后数据基本都缺一块。2. 能不能恢复先看这三张底牌恢复这件事很像打牌手里有什么牌决定你打什么打法。别一上来就抄 binlog2sql 的命令先检查底牌。2.1 binlog 开着吗格式是 ROW 吗binlog二进制日志是 MySQL 记录所有变更操作的日志默认是关闭的。如果没开本文后面一半内容基本用不上如果开了还要看格式。binlog_formatROW记录的是每一行变更前后的完整镜像支持闪回工具生成反向 SQL是恢复 DML 误操作的最佳格式。binlog_formatSTATEMENT或 MIXED记录的是原始 SQL 语句闪回工具难以逆向出真实的行数据只能走备份binlog 重放。binlog_row_imageFULLROW 模式下记录了完整的前镜像和后镜像。如果值是MINIMALUPDATE 日志里只有被修改的字段没有整行旧值闪回出来的 SQL 很可能缺字段。我接手一个库时第一件事就是确认这三项。很多团队 binlog 开着但格式是 STATEMENT到了真出事才发现闪回工具跑不了那叫一个绝望。2.2 全量备份存在吗备份点离事故点多远备份是你恢复的地基。哪怕 binlog 齐全没有备份也要硬扛很长时间的重放甚至无从下手。检查备份时关注三点最近一次全量备份是什么时候备份点离事故时间越近需要重放的 binlog 越少恢复越快风险越小。备份文件里有没有记录 binlog 位点mysqldump 加--master-data2会在文件头写MASTER_LOG_FILE和MASTER_LOG_POSXtrabackup 会在备份目录生成xtrabackup_binlog_info文件。没有位点信息你都不知道该从哪条日志开始重放恢复难度陡增。备份文件验证过没有我遇到过备份文件损坏、少表、乱码的情况全是在演练时发现的。如果你从没做过一次恢复演练那这份备份的价值要打折扣。2.3 根据底牌选恢复路线底牌组合首选恢复方案说明binlogROW 有较新备份备份PITR或 binlog2sql 闪回误操作区间DML 误操作优先闪回DDL 误操作必须 PITRbinlogROW 无最近备份binlog2sql 闪回只能救 DML还要确认 binlog 覆盖事故区间binlogSTATEMENT 有备份备份PITR闪回工具基本不可用有备份 binlog 没开直接恢复备份会丢失备份点之后所有数据认命补数无备份 binlog 没开文件级恢复 / 第三方数据救援看运气成功率取决于磁盘有没有被覆盖一句话总结DML 误删误更新最优雅的方案是 binlog 闪回DDL 误删表必须备份PITR没有 binlog 也没有备份那就只剩极限操作下一篇再细讲。3. 基于binlog的闪回恢复把 delete 和 update倒着演一遍先说一下闪回的核心思路binlog 里记录了每一条变更的原始内容误 DELETE 的行日志里有完整行数据我们把它转成 INSERT 再执行回去误 UPDATE 的行日志里有修改前和修改后两个镜像我们把它转成一条反向 UPDATE将值改回原样。这就是把 SQL 倒着演一遍。3.1 用 mysqlbinlog 先定位事故 SQL假设事故发生在 2024-04-12 10:32 左右当前库的 binlog 文件是mysql-bin.000014。先用 mysqlbinlog 把事故时间段的日志解析出来mysqlbinlog --no-defaults --base64-outputDECODE-ROWS -v \ --start-datetime2024-04-12 10:20:00 \ --stop-datetime2024-04-12 10:50:00 \ /var/lib/mysql/mysql-bin.000014 /tmp/error_range.sql这一步不执行任何写操作放心跑。打开/tmp/error_range.sql在文件里找### DELETE FROM或### UPDATE标记就能看到带表名、带行数据的伪 SQL### DELETE FROM testdb.orders ### WHERE ### 110086 ### 22024-04-12 09:15:00在每条伪 SQL 前面有一行# at 3801这样的内容# at后面的数字就是这条事件在 binlog 文件里的 position。把事故 SQL 的# at位置记下来比如 3801再把下一条正常事件的位置也记下来比如 4058这两者就圈定了误操作的精确区间。注意时间过滤只是粗筛最终定位一定要看 position不要只看时间。应用服务器和数据库服务器时钟可能有偏差后面我会单独讲这个坑。3.2 用 binlog2sql 自动生成回滚 SQL手工从解析文件里拼 INSERT/UPDATE 也能恢复但表大了会疯掉。专业做法是用 binlog2sql 工具它是专门把 binlog 解析成 SQL 的开源工具。装好环境git clone https://github.com/danfengcao/binlog2sql.git cd binlog2sql pip install -r requirements.txt先用正向模式确认解析出来的 SQL 是否就是事故语句python binlog2sql.py -h127.0.0.1 -P3306 -uroot -p你的密码 \ --start-filemysql-bin.000014 \ --start-position3801 --stop-position4058 \ -d testdb -t orders --sql-typeDELETE参数说明--start-file指定 binlog 文件--start-position/--stop-position指定精确区间。-d指定库-t指定表--sql-type可以只刷 DELETE、UPDATE 或 INSERT减少干扰。注意运行 binlog2sql 的账号只需要SELECT、REPLICATION SLAVE、REPLICATION CLIENT权限别拿生产 super 账号跑。确认 SQL 没问题后加-B生成回滚 SQLpython binlog2sql.py -h127.0.0.1 -P3306 -uroot -p你的密码 \ --start-filemysql-bin.000014 \ --start-position3801 --stop-position4058 \ -d testdb -t orders -B /tmp/orders_rollback.sql打开/tmp/orders_rollback.sql检查一下开头应该是对应误删行的 INSERT 语句。这里要花两分钟人工核对几条关键数据确认它就是你要恢复的内容。3.3 执行回滚和验证执行回滚前再确认一次原库状态是只读防止恢复过程中又有外部写入。然后导入mysql -uroot -p testdb /tmp/orders_rollback.sql数据量小的话几秒就完事几十万行回滚可能要几分钟建议业务低峰执行。执行完后立刻验证SELECT COUNT(*) FROM testdb.orders; SELECT * FROM testdb.orders WHERE id IN (10086, 10087, 10088);重点查两类值总行数是否恢复、事故更新/删除的字段值是否回到预期。如果回滚执行一半报了主键冲突说明区间选错了或者并发有写入别硬着头皮继续停下来重新定位区间。提示binlog2sql 只处理 DML不处理 DROP、TRUNCATE 这类 DDL。误删表结构必须走下一章的备份PITR。4. 备份 binlog 时间点恢复最稳妥的全量救法闪回适合单表、单一误操作。如果你误删的是整个库、误 TRUNCATE 了表或者误操作时间跨度大、影响行数无法估量就走备份PITR。这条路线思路也很直白把全量备份恢复到临时实例然后把备份点到事故前一刻的 binlog 重放进去最终得到事故前 1 秒的完整数据。4.1 把全量备份恢复到临时实例先准备一台和原实例同版本的 MySQL版本不一致binlog 重放容易出兼容问题然后找最近的全量备份。假设备份文件是 /backup/testdb_full_20240411.sql它是个 mysqldump 备份而且带了 binlog 位点信息CREATE DATABASE testdb DEFAULT CHARACTER SET utf8mb4;导入mysql -uroot -p testdb /backup/testdb_full_20240411.sql导入后查备份里记录的 binlog 位点grep CHANGE MASTER /backup/testdb_full_20240411.sql输出类似-- CHANGE MASTER TO MASTER_LOG_FILEmysql-bin.000013, MASTER_LOG_POS154;这告诉我们这个备份文件对应mysql-bin.000013的 154 位置。后面重放 binlog 就要从这个文件、这个位置开始。如果是 Xtrabackup 物理备份则看备份目录里的xtrabackup_binlog_info内容同样是一个文件名 position。4.2 从备份位点重放 binlog 到事故前 1 秒现在需要把mysql-bin.000013从 position 154 开始重放中间可能还有mysql-bin.000013、mysql-bin.000014等多个文件一直重放到事故 SQL 的前一个 position上一章定位的 3801 之前。重放第一个文件mysqlbinlog --no-defaults --skip-gtids \ --start-position154 \ /var/lib/mysql/mysql-bin.000013 | mysql -uroot -p testdb如果中间隔了多个 binlog 文件就逐个执行注意顺序不能乱。重放事故文件mysql-bin.000014时用 stop-position 掐在事故 SQL 之前mysqlbinlog --no-defaults --skip-gtids \ --start-position4 --stop-position3801 \ /var/lib/mysql/mysql-bin.000014 | mysql -uroot -p testdb这里我一直强调--skip-gtids。因为原库如果开了 GTIDbinlog 里带的是已经执行过的 GTID直接重放到临时实例会报 impossible GTID 之类错误跳过 GTID 后它会当作普通日志重放临时实例再自己生成新的事务标识。重放完成后检查临时实例SELECT COUNT(*) FROM testdb.orders;对比业务方记录的事故前行数再抽查几行关键数据。确认没问题后导出临时库数据mysqldump --single-transaction -uroot -p testdb /tmp/testdb_recover.sql导回原库或者有条件的话直接让应用切到这个临时实例作为新库。4.3 多 binlog 文件、GTID 和单表恢复的注意点有几个细节在实际操作中特别容易栽跟头多个 binlog 文件要逐个重放。mysqlbinlog 虽然支持一次传多个文件但--stop-position只对最后一个文件生效中间文件会整个重放容易放过头。老老实实一个文件一个文件来每个文件确认无误再放下一个。只恢复单张表时别直接对生产库玩 PITR。最安全的做法是临时实例整体恢复到备份点再把目标表单独导出/导回。单表恢复也可以配合上一章的 binlog2sql 按-d 库 -t 表提取但那要求 binlog 格式是 ROW。恢复过程中临时实例上也别忙着开写。如果临时库又写了新数据你再导出导入就混入了脏数据恢复结果就不干净了。重放速度慢是正常的。如果备份点离事故点相隔几天的 binlog重放可能要几小时甚至更久。这时候耐心等宁可慢也不要跳过日志。5. 没有binlog也别急着放弃文件级恢复的极限操作刚讲了闪回和 PITR这俩都依赖 binlog。那如果 binlog 压根没开备份又停留在三天前数据就彻底没救了吗不一定还有一条文件级的路但成功率很看运气而且只适用于特定场景。5.1 什么时候文件级恢复能成先把话说清楚避免你抱错希望。文件级恢复通常能成的场景是操作系统的文件被 rm 掉了但 MySQL 进程还活着。比如有人在服务器上不小心执行了rm /var/lib/mysql/testdb/orders.ibd把表空间文件删了但 mysqld 进程没有重启InnoDB 依然持有这个文件的句柄文件内容其实还留在磁盘上。相反如果是执行了DROP TABLE或TRUNCATE TABLEInnoDB 是在内部清理表空间文件被释放、空间被标记为可重用再用 /proc 那套基本没戏。这种只能靠备份PITR。所以文件级恢复是最后一搏不是常规武器。5.2 用 /proc 文件系统找回被误删的 ibd假设你误删了/var/lib/mysql/testdb/orders.ibdmysqld 还活着。第一步找到 mysqld 进程号和它打开的文件句柄pgrep -x mysqld lsof -p mysqld_pid | grep deleted输出里能看到类似这样的行mysqld 3341 mysql 10u REG 252,1 524288000 234567 /var/lib/mysql/testdb/orders.ibd (deleted)10u就是文件句柄编号把进程持有的这个已删除文件复制出来cp /proc/3341/fd/10 /tmp/orders.ibd复制出来的/tmp/orders.ibd就是表空间文件的完整内容。接下来把它恢复到新实例中先准备同版本 MySQL 实例创建同名库表表结构必须和原来一致。在新表上执行ALTER TABLE testdb.orders DISCARD TABLESPACE;这会删掉新表的 .ibd 文件。把/tmp/orders.ibd复制到新实例对应数据目录下注意修改属主chown mysql:mysql /var/lib/mysql/testdb/orders.ibd。执行ALTER TABLE testdb.orders IMPORT TABLESPACE;。执行完SELECT COUNT(*)验证一下数据一般能找回来。这个操作我在测试环境验证过多次关键前提就是进程别重启、文件别被覆盖。5.3 这招的边界和心态预期如果 mysqld 已经重启过文件句柄就没有了/proc 方案直接作废。剩下的是通用文件恢复工具去扫磁盘找那些被标记为已删除但未覆盖的数据页这种成功率完全取决于磁盘写入频率。生产环境高峰状态下几小时前被删的数据页很可能已经被覆盖得干干净净。所以我把文件级恢复定位成极限操作值得一试但别赌。真正靠谱的保障永远是 binlog 和备份。6. 恢复演练中踩过的坑每一个都能毁掉二次数据各种恢复方案讲完再分享几个我真实踩过的坑。这些坑单看都是小细节真遇到会发现一个比一个致命。6.1 binlog_row_image 没设 FULL回滚 SQL 缺字段我第一次帮人闪回 UPDATE 时就翻过车。当时 binlog 是 ROW 格式但binlog_row_image被设成了MINIMAL。结果 binlog2sql 生成的回滚 UPDATE 里只有被修改的那一个字段其他字段没有旧值可以还原。检查方法就一条命令SHOW VARIABLES LIKE binlog_row_image;正确值必须是FULL。如果不是闪回工具生成的 SQL 会残缺。遇到这种底牌别硬用闪回直接走备份PITR 还更稳。6.2 时间筛选遇时区偏差恢复漏了最后几秒用--start-datetime和--stop-datetime过滤 binlog 时时间基准是数据库服务器事件提交的本地时间不是应用上报的时间也不是你客户端的时间。应用说我 10:32 误操作的DB 的SELECT NOW()可能已经 10:35你拿 10:32 做 stop-position轻松就把事故前最后几秒正常事务漏掉。我的习惯能用 position 绝不用时间必须用时间就把范围放宽到前后 2 分钟解析出来后再靠# at位置人工精确定位。别省那几分钟解析时间漏了数据一夜白干。6.3 GTID 未跳过、自增计数错乱PITR 重放 binlog 时忘记加--skip-gtids恢复会在第一条事务就报错中断。这个上面已经强调过不再重复。另一个隐蔽问题是自增主键。从备份恢复到临时实例后表的AUTO_INCREMENT计数可能停在了备份时点比当前最大 id 还小。如果你把这份数据导回原库业务继续写入先是主键冲突后面可能引发主从延迟甚至复制中断。恢复完成后要检查SELECT AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMAtestdb AND TABLE_NAMEorders;如果明显小于MAX(id)1就在临时实例上执行ALTER TABLE orders AUTO_INCREMENTmax_id1;修正。记住这个操作要在临时实例上做别在导回后对着原库做否则又会产生一堆额外 binlog。6.4 一套值得抄走的日常防御配置每次恢复做得再漂亮都不如把这几个配置提前落实配置项推荐值作用binlog_formatROW保证行级镜像闪回工具可用binlog_row_imageFULL记录完整前镜像防止回滚缺字段binlog_expire_logs_seconds至少覆盖备份周期保证 PITR 有日志可重放全量备份频率建议每日备份点越近恢复越快客户端safe-updates开启UPDATE/DELETE 不带 WHERE 直接拒绝执行关于最后一项多说一句MySQL 客户端启动时加--safe-updates就能阻止不带 WHERE 条件的 UPDATE/DELETE很多误更新全表的悲剧都能从源头避免。生产环境里高危账号如果实在没法最小权限至少给开发同学配上这个客户端参数。我现在接手任何一个新库第一件事就是检查上面三行配置。数据恢复这事说到底拼的不是运气是平时有没有把这些底牌准备好。真出了事冷静下来按这套流程走大部分 DML 误操作都能救回来。
返回列表