第8章:在完整事务边界停止回放
误删订单后,最直接的想法是恢复到删除之前一秒。但日志时间戳的精度、服务器时区、长事务和并发提交会让这个说法含糊。精确恢复更需要一个能明确排除错误事务的边界。本章在单一binlog文件中,用真实提交之后的字节位置保存两笔合法交易,排除误删及其后变化。
先定义目标集合
基线包含初始订单和并发备份阶段结束后的全部合法订单。之后源库提交订单二千零一和二千零二,金额分别十七和二十三。这两笔都需要保留。程序立即保存完整业务状态和SHOW BINARY LOG STATUS返回的位置,然后才执行删除事务。目标因此明确为基线加两笔合法交易,而不是“看起来接近事故前”。
错误事务删除全部订单和分录,将账户余额设回十万。它在数据库内部依然是一个完整、一致、成功提交的事务。随后再新增一笔误删后订单。恢复目标排除这两个阶段,既不能把错误删除带回来,也不能把后来的业务当成已自动修复。生产中是否保留事故后合法订单,需要单独业务评估,本书不自动合并。
开始位置和结束位置都有来源
开始位置来自mysqldump注释中的SOURCE_LOG_FILE和SOURCE_LOG_POS,表示基线之后应继续读取的日志位置。结束位置来自两笔合法事务提交成功后的服务器状态。文件名必须相同,否则程序中止,因为当前实验只实现单文件范围。我们没有用硬编码数字假装所有MySQL日志布局都一样。
mysqlbinlog读取start-position到stop-position指定的事件范围,输出可执行内容,再由mysql输入目标实例。结束点是最后一笔保留事务之后的边界,错误事务尚未开始,所以不会截到半个业务事务。日志里可能包含格式、GTID和会话设置事件,恢复代码交给官方工具解释,而不是自行解析字节拼成SQL。
为什么不手工过滤几张表
订单、分录和余额共同构成一项业务。如果只回放orders而忽略accounts,恢复结果可能显示订单完整,却无法结算。按数据库或表过滤还涉及跨库事务、默认数据库和ROW事件的解释条件。过滤规则需要专门设计,不能因为操作方便就默认安全。本书选择一段连续事务前缀,保留原始事务整体。
同理,挑几条DELETE的反向INSERT并不等于一般恢复。原值可能缺失,后续外键和唯一约束可能已变化,外部支付或消息副作用也不会随SQL倒放而撤销。业务修复可以有针对性的补偿流程,但那是另一种工具,应保留审计和独立验收,不能与日志前缀恢复混为一谈。
目标实例的GTID历史
基线导入没有写入源全局GTID集合,目标实例会有自身导入产生的事务身份;回放保留源日志的GTID事务。这个组合服务于独立历史恢复,而不是直接声明目标可以回到原生产拓扑。重新加入复制需要检查GTID集合、数据范围和额外本地事务,本书没有自动完成这个动作。
因此验收以业务完整状态为中心,源目标GTID被记录为定位信息。不能只查询GTID_SUBSET为真就宣布订单正确,因为误操作、跳过事务或错误初始化都可能让事务历史与业务数据含义分离。副本自动定位会在后面的章节单独讨论。
三组断言一起成立
首先,回放后的订单、余额与分录摘要必须等于之前保存的目标。其次,误删后订单三千零一必须不存在,证明没有把结束位置选得太晚。最后,源在回放期间确实停止,输入来自独立归档,证明恢复路径没有向在线源借状态。缺少其中任何一组,结论都会比实际证据更强。
日志摘要也写入报告,但它标识的是回放输入,不是成功条件。不同运行的实例UUID和日志时间会改变字节,合法的回放摘要可能不同;业务目标则可按相同订单构造稳定比较。读者应区分工件身份与业务等价,避免把每次实验文件hash不同误判成数据不一致。
扩展到生产跨文件恢复
真实恢复可能跨越很多日志文件。开始文件从基线坐标起读,中间文件完整回放,最后文件在目标事务边界停止。需要核验顺序、来源身份、校验和、归档缺口与事务完整性,并保持目标隔离。先在副本环境验证范围,再开放流量。本书把这条路径缩成单文件,是为了清楚暴露原则,不是声称跨文件恢复无需额外工作。