一个数据库副本能打开,quick_check也返回ok,仍可能不是你以为的那一份备份。SQLite采用WAL模式时,已经提交的变化可能还在旁边的日志文件里;直接复制主文件,得到的可能是一份结构正常、内容却落后的数据库。
本文给维护小型SQLite工具的开发者提供一次可复现的恢复演练:制造尚未checkpoint的新记录,对照主文件复制与Backup API,再重新打开副本检查具体数据。所有记录都在临时目录生成,不连接生产数据库,也不把这次SQLite实验说成本站数据库恢复测试。
下载完整实验包:备份脚本、WAL对照运行器、观察结果与12项测试(ZIP)
先看一份“能打开”的旧副本
解压进入sqlite-backup-v1目录,运行:
python3 run_lab.py
python3 -m unittest -v
需要Python 3.11+与sqlite3模块,文件发布步骤面向支持硬链接的可信本地目录。本次实测macOS、Python 3.14.4、SQLite 3.53.2;Python版本和它链接的SQLite版本要分别记录。
运行器先创建WAL模式的库存表,提交A=10并执行checkpoint。接着提交A=20、新增B=5,保持连接打开,此时新变化仍在WAL中。再做两份副本,结果如下:
| 检查对象 | quick_check | 恢复后查询 |
|---|---|---|
| 源库当前状态 | ok | A=20,B=5 |
| 只复制主文件 | ok | A=10,没有B |
| Backup API副本 | ok | A=20,B=5 |
这组结果来自实际SQLite查询。第二行的ok没有说谎:它并不承诺副本已经包含源库最近提交的每一项业务变化。因此验收不能停在“复制完成”或“数据库检查通过”,还要确定副本应恢复到什么状态。
为稳定重现差别,运行器只在自己的临时库中关闭自动checkpoint。这个设置不是给生产库的建议。演示结束后会关闭连接并清理实验目录;不需要对现有文件执行删除或覆盖。
WAL是数据库状态的一部分
SQLite官方WAL说明明确指出,WAL可能包含已经提交的事务,移动或复制数据库时不能把它当作可随意丢弃的临时文件。主文件与WAL分离,可能丢失已提交内容,也可能造成损坏。
本例故意保留源连接,并在复制前确认WAL非空。它展示的是这个状态下的主文件复制风险,不是声称任何场景下复制数据库文件都必然失败。应用正常关闭后的checkpoint与文件状态是另一组条件;仅仅看不到应用窗口,也不能证明所有连接已经正确关闭。
不要把修正方法理解成“按顺序再复制db、wal、shm三个文件”。活动数据库在两次文件读取之间仍可能改变,分别复制文件并不会自动构成同一个一致时间点。更稳妥的起点是使用数据库支持的备份机制,并明确恢复验证流程。
让SQLite完成复制,脚本负责结果发布
本包通过Python的Connection.backup()调用SQLite Backup API。源库用只读连接打开,路径不存在就失败,不会悄悄新建一个空数据库。目标先是本次专用的临时文件,而不是最终交付路径。
source_connection.backup(
destination_connection,
pages=16,
progress=check_progress,
sleep=0.01,
)
这是关键调用片段,完整脚本包含连接关闭、异常清理和发布步骤。复制完成后,程序对副本运行PRAGMA quick_check,再仅把副本改为DELETE日志模式,关闭连接后同步文件,最后生成SHA-256。源库仍保持原来的WAL模式。
这里选择单文件副本,是为了让本次恢复材料不依赖旁边的目标WAL文件。这个选择作用于副本,不能拿它作为直接改生产数据库日志模式的理由。只读WAL连接也可能需要可读或可创建的辅助文件,因此“只读连接”不等于所有文件系统活动都为零。
旧备份不能被一次失败的新备份覆盖
单独使用脚本时,目标目录须已存在,目标文件名应是新的:
python3 backup.py source.db new-snapshot.db
成功时退出0并输出副本路径、字节数、摘要和检查结果。读取或发布失败时退出1。目标已存在时,程序拒绝覆盖;这使调用方能够保留前一份可用备份,而不是因为定时任务重跑就失去它。
发布使用同一目录中的硬链接独占创建最终名称。临时文件已完成写入和检查后才出现最终路径;已存在的普通文件或符号链接都不会被替换。文件系统不支持这一步就报错,不退回会覆盖目标的实现。
测试在复制进度回调中主动中断,以及在发布时注入文件系统错误,确认没有发布半份副本,并清理本次临时文件。已有目标的原始字节保持不变。该保护只针对正常异常处理路径;强杀、断电或清理动作自身失败仍需要外部巡检,本次没有测试这些情况。
代码同步了副本文件,但没有同步父目录,不承诺所有掉电情形下的持久性。进度回调中的时间预算也是协作式检查,不是能够打断任意磁盘阻塞的硬时限。目录应由自己控制,本例没有处理不可信本地进程替换路径的竞态。
重新打开副本,核对你真正需要的数据
本次恢复验证用新的连接打开副本,运行如下业务查询,而不是复用原来的连接对象:
SELECT sku, quantity FROM items ORDER BY sku;
预期值是A=20和B=5。脚本随后把源库A更新为30,再打开副本,副本仍为A=20。这说明交付的是该次备份形成的快照,不是之后自动跟随源库变化的副本。
| 验证层次 | 本次证据 | 不能据此推出 |
|---|---|---|
| 文件 | 副本12288字节,生成SHA-256 | 文件来源可信、业务数据齐全 |
| 数据库 | 新连接打开,quick_check=ok | 包含所有应有的新提交 |
| 业务样本 | 读取A=20、B=5 | 其他表、外部文件和业务流程均正确 |
真实项目应把这张表里的业务样本换成自己的关键对象,例如一条已确认提交的记录、一个版本标记及必要关联关系。只有自己预先知道应恢复什么,查询结果才有可判断的含义。不要让备份程序临时挑几条“看起来正常”的数据充当验收标准。
这份实验覆盖到哪里
12项测试覆盖WAL新数据恢复、主文件复制反例、源数据保持、备份后新写入不进入快照、已有目标保护、源路径错误、同路径拒绝、进度中断、发布失败、损坏源库、摘要与单文件结果,以及真实命令行执行。完整结果留在包中。
备份过程中源连接保持打开,但没有做持续并发写入的压力测试,不承诺某一台机器上的速度或精确起始时刻。大数据库、网络盘、Windows、加密数据库及多库ATTACH事务都未验证;本工具仅备份main数据库,不包含外部文件、配置或密钥。
副本应存放在受控位置,不在公开网站目录发布真实业务数据库。本文提供的公开下载只有脚本与合成实验结果。加密、异地副本、保留周期、恢复耗时目标还需要按真实系统另行设计。
源码包SHA-256:dc152d6b467c0009502e9d0f2234ce1a0d007cd59aed35a2fc2e186c3624b198,MIT许可。封面为AI生成的数据库复制与核验概念图,正文表格对应本次运行记录。
依据与前置阅读
- SQLite Online Backup API说明
- SQLite WAL、checkpoint与文件状态
- Python sqlite3.Connection.backup
- SQLite导入计划与事务回滚
- 网站恢复演练需要哪些检查
- 备份材料的存放与公开访问边界
资料查阅与编辑日期:2026年10月30日。作者:玖柒。








