ARTICLE DETAIL

资讯详情

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

SELECT FOR UPDATE 误删数据别慌:事务回滚、binlog 闪回与备份恢复全解析

SELECT FOR UPDATE 误删数据别慌:事务回滚、binlog 闪回与备份恢复全解析 先说结论能但前提是你给数据库留了“后悔药”。我处理过不少类似的求助场景几乎都是同一个套路——开发同学为了办事稳妥先跑了一条SELECT * FROM orders FOR UPDATE把要操作的订单锁住核对完业务状态后顺手DELETE了一条结果事务一提交才发现删错了对象。这时候能不能恢复取决于三个关键状态事务提交了没有、数据库开没开 binlog或归档、有没有全量备份兜底。这篇文章就把这三个状态对应的恢复方案全部盘一遍照着操作就行。1. 先搞懂 FOR UPDATE 删数据到底是个什么场景1.1 FOR UPDATE 锁的到底是什么SELECT ... FOR UPDATE是数据库的悲观锁机制。以 MySQL InnoDB 为例这条语句会对扫描到的行加排他锁X 锁锁住之后其他事务既不能修改这些行也不能对这些行加FOR UPDATE/FOR SHARE锁直到当前事务提交或回滚。如果你写的是SELECT * FROM table FOR UPDATE没有 WHERE 条件那么扫描到的所有行都会被锁定效果上等同于锁全表。但有个关键点很容易被忽略这个锁只约束“其他事务”不约束“你自己”。在同一个事务里你已经通过FOR UPDATE锁定了行接下来完全可以对这些行执行DELETE或UPDATE。很多人误以为FOR UPDATE锁定之后数据就“很安全”实际上它只是防止别人改不防止自己改错。文章标题里描述的场景本质上就是“自己先加锁然后自己删”数据库不会觉得这有什么问题如果你还执行了COMMIT那这行数据的变更就算正式落盘了。1.2 数据被 DELETE 之后物理层面去了哪里要理解“能不能恢复”先要搞清楚 DELETE 之后数据是不是真的没了。答案是否定的数据不会立刻从磁盘上被“擦除”。在 MySQL InnoDB 中DELETE只是给数据页里的行记录打上删除标记deleted mark并写入 undo log 保存数据的旧版本。如果事务没提交undo log 里保存的是完整的前镜像可以直接用于回滚如果事务已提交数据页上的物理记录仍然存在只是对事务不可见后续由 purge 线程在合适的时机清理。在 Oracle 中DELETE也类似数据块中的旧版本会保存在 undo 表空间中供一致性读和闪回使用。binlogMySQL、redo logMySQL/Oracle、归档日志Oracle里则记录了完整的变更历史包括被删除行的内容。所以“数据恢复”的本质是把数据还在数据库内部某处的“残留形态”重新捞出来或者把日志里记录的变更历史反推回去。后面所有方案都是围绕这些信息源展开的。2. 恢复前的关键判定能不能恢复先回答这三个问题2.1 当前事务提交了吗这是最重要、也最容易被忽略的问题。很多人在执行DELETE后发现删错了第一反应是去翻备份、找恢复工具结果折腾半天最后才发现事务根本没提交一条ROLLBACK就能解决。所以拿到这种求助我第一句话永远是先看动手的那个连接还在不在事务有没有提交。判断方法很简单如果是命令行窗口直接看一眼窗口里有没有执行过COMMIT没有就立刻输入ROLLBACK。如果是应用代码去查日志、看慢查询或SHOW PROCESSLIST确认这条 DELETE 所在的事务是否已经结束。也可以用系统视图查正在运行的事务-- MySQL SELECT * FROM information_schema.INNODB_TRX\G -- Oracle SELECT * FROM v$transaction;如果查得到当前事务说明连接还没提交恢复基本是 100% 成功如果查不到说明事务已经结束再往下走日志恢复流程。2.2 数据库类型和日志备份状态如何确定了事务已提交之后能不能恢复就要看数据库的“家底”了。我整理了一个判定表格可以直接照着对照场景数据库必要条件推荐恢复手段成功概率事务未提交MySQL / Oracle连接未关闭、未 COMMITROLLBACK回滚整个事务接近 100%已提交MySQL开启 binlog 且为 row 格式binlog2sql 反向生成 INSERT很高已提交MySQL有最近全量备份 开启 binlog备份恢复 binlog 前滚到删除前位置很高已提交MySQL无备份、无 binlogInnoDB 物理数据页工具尝试中低已提交Oracle处于 UNDO 保留期内Flashback Query / Flashback Table很高已提交Oracle超出 UNDO 保留期RMAN 归档日志做时间点恢复中高已提交PostgreSQL开启 WAL 归档PITR 时间点恢复很高已提交PostgreSQL无 WAL 归档基本无解依赖外部备份极低注意表格里 MySQL 那两行都要求开启 binlog。如果你用的 MySQL 没开 binlog恢复窗口会非常窄基本只能寄希望于物理数据页还没被 purge 清理或者有外部备份。2.3 应急三查三十秒内锁定方案为了快速定位我通常建议按这个顺序操作三十秒内就能判断出该走哪条路查事务状态SHOW PROCESSLIST;和information_schema.INNODB_TRX确认连接是否还活着、事务是否未提交。查 binlog / 归档状态MySQL 执行SHOW VARIABLES LIKE log_bin;和SHOW VARIABLES LIKE binlog_format;Oracle 执行SELECT log_mode FROM v$database;。查备份策略去看备份系统里最近一次全量备份是什么时间、能不能用、有没有额外的 binlog 备份或归档备份。这三查做完恢复方案基本就定了。最怕的情况是事务已提交 没开 binlog 备份是两周前的。这种情况也不是完全没救但就要走物理数据页恢复这类偏门路线了。3. 最简单的情况事务没提交直接 ROLLBACK 回滚3.1 ROLLBACK 操作的正确姿势如果确认事务还没有提交那恭喜你这是一道送分题。直接在原来的会话里执行ROLLBACK;执行之后事务里的所有修改都会被撤销包括那条DELETE。这里有两个操作细节必须注意不要关掉连接更不要“手贱”先开一个新会话去查数据。当前连接如果断开了数据库可能会自动回滚未提交事务InnoDB 在连接断开时会清理未提交事务这本意是好的但有些情况下你要重新找这个事务反而增加判断成本。稳妥做法是保持连接不断直接回滚。ROLLBACK 会回滚整个事务不是只回滚最后一条 DELETE。如果你的事务里除了这条 DELETE 还执行了其他 UPDATE、INSERT这些操作也会一起被撤销。如果那些操作是有效的业务变更你要提前想好回滚后需要重放哪些操作别救回一条数据、丢掉一批数据。3.2 为什么不建议“手动 INSERT 补一条”有些同学不知道可以回滚删完之后直接拿原来的值手动INSERT一条回去。我特别不建议这么干原因有两个自增主键、创建时间、更新时间这类字段很容易和原值不一致尤其是自增 ID手动插入会导致后续主键分配错乱。如果表里有触发器、外键约束或者与缓存联动手动插入触发的是“新插入”的完整链路和原记录的初始状态不同容易产生隐藏问题。所以事务没提交时ROLLBACK才是正经答案手动补数据是实在没办法时才考虑的兜底写法。4. 已提交误删MySQL用 binlog 把数据捞回来4.1 先确认 MySQL 的 binlog 状态一旦确认事务已提交MySQL 场景下的恢复核心就变成 binlog。先执行下面三条 SQL确认你有没有这张“底牌”SHOW VARIABLES LIKE log_bin; SHOW VARIABLES LIKE binlog_format; SHOW BINARY LOGS;log_bin为ON说明 binlog 已开启有戏。binlog_format建议是ROW。如果是ROW格式binlog 里会记录每一行变更的前后镜像恢复时可以直接反推出被删除的行内容如果是STATEMENT格式只记录 SQL 语句恢复难度会大很多。SHOW BINARY LOGS能看到当前有哪些 binlog 文件确认删数据时对应的文件还在不在。如果文件被清理了那 binlog 这条路就堵死了只能看备份或物理文件。另外提醒一句如果你的 MySQL 是云厂商的 RDS一般会自动开启 binlog并在控制台提供“按时间点恢复”或“库表级恢复”功能这种场景直接使用云厂商的恢复功能往往比自己下载 binlog 更省事。但原理和下面讲的是一致的。4.2 定位误删 DELETE 在 binlog 中的位置知道 binlog 存在还不够你得知道误删的那条 DELETE 具体在哪个文件、哪个位置。定位方法通常是先估算大致时间然后用mysqlbinlog工具解析出该时间段的日志内容。假设误删发生在2024-06-01 10:20:00左右binlog 文件是mysql-bin.000003解析命令mysqlbinlog --no-defaults \ --base64-outputDECODE-ROWS \ -v \ --start-datetime2024-06-01 10:15:00 \ --stop-datetime2024-06-01 10:25:00 \ /var/lib/mysql/mysql-bin.000003输出内容里你会看到类似这样的片段### DELETE FROM testdb.t_order ### WHERE ### 112345 ### 22024-05-30 10:00:00 ### 3华为Mate 60 ### 45999.00这段信息非常关键1、2、3分别代表被删行各个字段的值后面恢复时能直接用。你能看到的字段数量和顺序取决于表结构和binlog_row_image参数。如果参数是FULL默认值所有列都会被记录如果是MINIMAL只记录主键和变更列恢复时可能缺少必要数据所以在常规实践中推荐生产环境使用binlog_row_imageFULL。4.3 推荐直接上 binlog2sql反向生成 INSERT 语句手动从 mysqlbinlog 输出里拼 INSERT 语句不是不行但很累而且容易出错。我处理这类问题最常用的开源工具是 binlog2sql它能把 ROW 格式的 binlog 反向解析成对应的 SQL 语句比如把 DELETE 解析成 INSERT把 UPDATE 解析成反向 UPDATE。这个思路非常直接适合单表单行或单表小范围恢复。安装也很简单git clone https://github.com/danfengcao/binlog2sql.git cd binlog2sql pip install -r requirements.txt然后运行反向解析例如恢复testdb.t_order表中在指定 binlog 区间内被删的行python binlog2sql/binlog2sql.py \ -h127.0.0.1 -P3306 -uroot -p123456 \ -d testdb -t t_order \ --start-filemysql-bin.000003 \ --start-datetime2024-06-01 10:15:00 \ --stop-datetime2024-06-01 10:25:00 \ -B rollback.sql关键参数说明-B/--flashback生成反向 SQLDELETE 会变成 INSERTUPDATE 会还原成修改前的值。没有这个参数解析出来的是原始 DELETE不能用。--start-file指定从哪个 binlog 文件开始解析。--start-datetime/--stop-datetime控制时间范围尽量把时间范围缩到最小避免生成大量无关 SQL。生成的rollback.sql打开以后应该是被删行的 INSERT 语句。执行前先打开文件人工确认一下数据内容有没有明显异常再执行mysql -uroot -p rollback.sql如果被恢复的表有自增主键直接用生成的 INSERT 语句会把原来的 id 一起插回去这样不会影响后续自增序列。但如果 id 已经和其他新数据冲突比如删除之后又有新的行占用了同样的自增 id就要根据实际情况调整。关于冲突处理后面专门讲。4.4 没有 binlog2sql 时用 mysqlbinlog 手工恢复也能做如果你的服务器不方便安装 Python 工具或者 binlog 是 STATEMENT 格式就需要走“备份恢复 binlog 重放”这条路。原理是先恢复到一个最近的备份点然后把对应 binlog 从备份点重放到误删前的最后位置这样数据就能回到删除前的状态。假设你有2024-06-01 00:00:00的物理备份或逻辑备份误删发生在10:20:00完整的恢复流程如下用备份恢复出一个临时实例千万别在原库上操作。物理备份可以直接替换数据目录逻辑备份用mysql backup.sql导入。把从00:00:00到10:20:00的 binlog 重放到临时实例上但要精确定位到删除前的位置。定位删除位置的方法先用mysqlbinlog解析出删除事务前后的# at pos标记找到DELETE FROM t_order这条事务的起始 pos。假设删除事务的起始位置是1945那重放时就重放到--stop-position1945mysqlbinlog --no-defaults \ --start-datetime2024-06-01 00:00:00 \ --stop-position1945 \ /var/lib/mysql/mysql-bin.000003 | mysql -uroot -p重放完成后临时实例里的数据就是“删除前”的状态。接下来把需要的几条记录从临时实例导出再导入原库。这里最耗费精力的就是定位 pos 点。我的经验是先看删除语句在 mysqlbinlog 解析结果里的# at标记记录下行号然后用sed或编辑器的搜索功能找到那条DELETE FROM t_order之前最近的一个COMMIT位置那个事务结束的 pos 就是可以安全停下的位置。宁可多往前停一点也不要停在删除事务中间否则恢复出来的数据不完整。4.5 恢复后的一致性检查清单数据插回去之后别急着宣布“搞定”一定要做几项检查被删的行数对不对SELECT COUNT(*) FROM t_order WHERE ...和删除前的业务数字对一下。主键是否冲突如果恢复时报Duplicate entry错误说明表里已经有同 id 的新数据需要先清理冲突行或调整恢复方式。关联数据是否同步比如订单删除后订单明细、日志表有没有跟着删如果外键级联删除生效那恢复订单的同时需要连明细一起恢复。业务缓存是否过期很多系统在更新数据库后会同步刷新 Redis 等缓存恢复完数据后如果缓存里还是被删状态可能导致线上读不到数据。所以恢复后最好重置相关缓存。前两条是数据层面的硬校验后两条是业务层面的软校验缺一不可。5. 已提交误删Oracle利用闪回特性快速恢复5.1 用 Flashback Query 查出被删数据Oracle 处理误删比 MySQL 要优雅得多因为它有原生闪回机制。前提是数据还在 UNDO 表空间中且处于UNDO_RETENTION保留期内。默认情况下Oracle 的UNDO_RETENTION是 900 秒15 分钟所以误删后要尽快处理。先查删除前的数据是否还能读到SELECT * FROM t_order AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL 10 MINUTE) WHERE order_id 12345;如果这条查询能返回被删的行说明 UNDO 里还有前镜像那恢复就非常简单。直接用子查询把数据插回去INSERT INTO t_order SELECT * FROM t_order AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL 10 MINUTE) WHERE order_id 12345; COMMIT;这种方式的优点是只影响目标行不影响其他数据。执行前可以先确认下UNDO_RETENTION的值SHOW PARAMETER undo_retention;如果事务提交时间已经接近或超过保留期可以临时把保留期调大再尝试ALTER SYSTEM SET UNDO_RETENTION 3600 SCOPEBOTH;但这个操作只对后续新生成的 UNDO 生效已经过期的事务不一定能救回来所以时间窗口非常重要。5.2 用 Flashback Table 整表回退如果误删的影响范围很大或者你想把整张表恢复到某个时间点可以用 Flashback TableALTER TABLE t_order ENABLE ROW MOVEMENT; FLASHBACK TABLE t_order TO TIMESTAMP (SYSTIMESTAMP - INTERVAL 10 MINUTE);这个操作会把整张表的数据回退到指定时间点该时间点之后对这张表的所有 DML 操作都会丢失。所以在执行前务必确认这段时间内没有其他有效业务变更或者先把这些变更单独导出备份。ROW MOVEMENT必须启用否则会报错。注意Flashback Table 回退的是整个表如果误删的行和正常业务写入都在同一个时间窗口内回退后可能连正常写入的数据也一起丢了。所以这个方案在单条误删场景下其实不如 Flashback Query 安全我一般只在整表数据损坏或批量误操作时才用。5.3 闪回不可用走 RMAN 归档恢复如果误删已经超过 UNDO 保留期或者数据库没有开启闪回相关配置那只能走 RMAN 时间点恢复。前提是数据库处于归档模式并且有可用的全量备份和归档日志。RMAN 的常见做法是把数据库恢复到误删前的时间戳。这个操作的破坏性比较大会覆盖整个数据库到目标时间点通常需要在克隆环境完成然后把需要的表数据导出再导回生产库。由于限流和时间原因克隆恢复的具体脚本我这里不展开写实际工作中有个原则一定要记住任何 RMAN 时间点恢复都必须在克隆环境验证成功之后再考虑对生产库动手。数据库层面的时间点恢复不是小事需要变更审批和业务方确认别自己半夜偷偷执行。6. 没有备份也没有日志还能怎么办极限场景6.1 从 InnoDB 物理数据页里“抠”数据最头疼的情况是MySQL 没开 binlog也没有任何备份事务已提交而且 purge 线程还没来得及清理被删行的物理记录。这种情况下依然有机会从物理数据文件里捞出数据但成功率完全看运气。InnoDB 表中被标记删除的行记录在数据页里仍然存在直到 purge 线程把它物理清除。如果数据量不大、删除后表没有大量写入物理记录可能在文件里躺很久。此时可以尝试 Percona Data Recovery Tool for InnoDB 这类工具直接读取.ibd文件解析行记录。但这类工具对 InnoDB 版本、行格式、压缩属性都有严格要求而且解析出来的结果经常是乱序或字段错位的需要你自己对照表结构去辨认。我个人的经验是这种方案非常“考古”能救回一部分数据就算成功不要抱有 100% 恢复的期待。如果你是在文件系统层面误删了整个.ibd文件那就进入更底层的数据恢复范畴可以尝试 extundelete、ext4magic 等工具前提是文件系统没被大量写入、被删文件的数据块还没被覆盖。这类操作最好提前把磁盘挂载为只读避免新的写入把残留数据块冲掉。6.2 坦白讲这种场景成功率不高重点是预防标题里问“能恢复吗”在没有任何日志和备份的情况下最诚实的答案是有机会但成功率不高而且恢复出来的数据不一定完整。我自己处理过的极限恢复案例里最终真正完整恢复的不到一半多数只能碎片化捞回部分数据。所以与其把希望寄托在极端恢复手段上不如在平时把防线建立起来。生产环境至少要做到这几条MySQL 开启 binlog并设置合理的保留周期比如 7 天binlog 格式设为ROWbinlog_row_image设为FULL。Oracle 开启归档模式定期做全量备份 归档备份。关键业务表至少每天一次逻辑备份备份文件异地留存。DELETE 前先SELECT确认影响行数重要操作先备份到临时表。创建只读账号给日常查询使用写权限收敛到最小范围。很多线上故障本来可以一分钟解决最后变成数小时甚至不可恢复都是因为平时省了这几步。7. 踩坑经验与高频问题速查7.1 恢复时最容易翻车的几个点第一个坑误删后继续在原库上执行大量写入。binlog 反向生成的 INSERT 需要原表没有主键冲突如果原表删掉的数据 id 已经被新数据占用那 INSERT 就会报Duplicate entry。所以发现误删后尽量先暂停写入或者至少暂停涉及那张表的业务。第二个坑binlog 文件被清理。很多环境设置了expire_logs_days或 binlog 自动清理策略如果误删后拖了很久才想起恢复binlog 可能已经被 purge。遇到这种情况要立刻去查备份系统里有没有 binlog 的异地备份没有的话只能碰运气看物理数据页。第三个坑时间范围定太宽。用 binlog2sql 反向生成 SQL 时如果时间范围包含其他正常事务生成的恢复脚本里可能会出现重复 INSERT 或多余的记录执行前务必人工核对。我一般在生成后先用文本编辑器打开确认里面只有需要恢复的那几条INSERT。第四个坑在事务里把 SELECT FOR UPDATE 和其他业务 SQL 混在一起回滚时把所有有效变更一起撤销。这个前面提过应对办法是在回滚之前把事务里需要保留的变更先记录成 SQL 脚本回滚后再重新执行。7.2 高频问题 QA 速查表问题快速回答已经过去 3 小时了还能恢复吗取决于 binlog 保留周期、UNDO_RETENTION、备份周期。先查日志还在不在在就有戏。云数据库 RDS 能像自建库一样操作吗云数据库一般提供自带的时间点恢复和库表级恢复功能优先用控制台的功能少自己折腾底层文件。恢复期间业务需要停机吗最小影响方案是不停业务把数据捞回后做比对但为了避免主键冲突建议先暂停对该表的写入。没有备份但 binlog 是开着的能恢复吗可以。只要 binlog 是 ROW 格式且保留周期内用 binlog2sql 反向生成 INSERT 即可。DELETE 之后还能用 SELECT FOR UPDATE 把数据查回来吗不能。FOR UPDATE只能查当前已提交的数据查不到已删除的行。恢复数据必须走日志或闪回。恢复后自增主键会不会乱恢复时带上原 id 就不会乱。如果主键已经被新数据占用需要先清理冲突数据或调整 id 后再插入。7.3 我给生产环境定下的操作规范因为踩过太多坑我给自己和团队定了几条硬规矩所有线上 DELETE 和 UPDATE必须先开启事务先SELECT确认影响行数确认无误后再执行写操作最后显式COMMIT。任何写操作完成后在监控里记录当前的 binlog 位置和时间点便于误操作时快速定位。除非是数据订正否则 DELETE 后不允许立刻关闭会话至少保留几分钟的观察期确认没问题再释放连接。涉及核心业务表的结构变更或大批量数据操作提前准备回滚脚本和数据备份。这些规矩看起来很基础但真正发生事故的时候能帮你省下大半天的恢复时间。我个人在实际操作中的体会是数据恢复这件事80% 的功夫在平时20% 才在事发后的操作。只要你把 binlog、备份、操作留痕这三样东西做好就算遇到“SELECT FOR UPDATE 后误删”这种低级失误也能在几十分钟内把数据找回来。最后再分享一个小技巧不管用 binlog2sql 还是 Flashback Query 恢复恢复 SQL 先放到一个事务里执行插入之前先查一遍是否已有冲突数据确认无误再统一提交比一条一条零散提交安全得多。
返回列表