JavaLYG

SQLite 备份恢复后缺少最新数据:用 Backup API 避开 WAL 裸复制陷阱

小型服务使用 SQLite,定时任务把 app.db 复制走,恢复后却少了最近的记录。文件没报错,备份时间也对,问题可能藏在旁边的 app.db-wal。本文面向本地磁盘上的 SQLite 服务,说明怎样改用在线备份、识别失败,并用隔离恢复确认数据,而不是只检查“文件还在”。

一、先确认:你备份的是文件,还是数据库状态

WAL 模式下,事务提交可以先把修改追加到日志,不必立即改写主数据库。读取时,SQLite 会结合主文件和 WAL 找到对应快照;checkpoint 才负责把日志中的内容搬回主文件。因此,正在运行时单独复制 app.db,可能得到能打开、结构也正常,却缺少最近提交记录的旧状态。

容易误判的是“程序已经提交,所以主文件一定最新”。提交与 checkpoint 不是一回事。也不要顺手删除 -wal:它不是随时可以清理的缓存。-shm 是相关共享内存索引文件,同样不要在连接活跃时手工处理。

SQLite 在线备份与隔离恢复四步图

二、先做三个检查,不要急着修文件

先确认应用真实打开的绝对路径、实际 journal_mode,以及备份脚本用的是 cp、压缩打包还是数据库接口。容器的挂载目录与宿主目录可能不同,备份到另一个同名空库也很常见。第三节示例用只读 URI 打开已有文件,路径写错时失败,避免新建空库;连接后可执行 PRAGMA journal_mode 核实模式。

如果返回 wal,按本文路线处理;如果不是 WAL,也不能推出运行中的裸复制天然安全。只读连接仍可能需要读取现有辅助文件或创建共享索引,权限不足不等于数据库损坏,别改成 immutable=1 来蒙混过关。WAL 不适用于跨主机网络文件系统。

三、在线备份交给引擎,别拼接三个文件

SQLite Online Backup API 会复制一致的数据库快照。Python 标准库 sqlite3 提供 Connection.backup,源库有其他连接访问时也可工作。逐页复制能缩短单次读取锁占用,但不是“零影响”:大量写入可能让备份反复重启,需要监控耗时与失败。

保存为 backup.py,在测试库目录运行 python3 backup.py。正式使用前核实源路径和目标目录权限,每次换一个新目标名。60 秒仅为预算示例。

from contextlib import closing
from pathlib import Path
import sqlite3
import time

源路径 = Path("app.db").resolve(strict=True)
目标路径 = Path("backup-001.db").resolve()
# 每次使用新文件,避免覆盖已有备份
目标路径.touch(exist_ok=False)
截止 = time.monotonic() + 60

def 检查进度(状态, 剩余页, 总页):
    if time.monotonic() > 截止:
        raise TimeoutError("备份超时,保留文件仅供排查")

with closing(sqlite3.connect(
        源路径.as_uri() + "?mode=ro", uri=True)) as 源:
    with closing(sqlite3.connect(目标路径)) as 目标:
        源.backup(目标, pages=256, progress=检查进度)
        assert 目标.execute("PRAGMA integrity_check").fetchall() == [("ok",)]
        assert not 目标.execute("PRAGMA foreign_key_check").fetchall()
print("备份完成,仍需隔离恢复与业务核对")

回调预算并非硬实时中断:单次底层调用仍可能耗时,调度器还应有独立超时。任何异常或非零退出都算失败,残留文件不要进入“可恢复备份”目录;成功后再归档与异地保存;这不是完整备份系统。

四、checkpoint 不是备份完成证明

有人会先跑 PRAGMA wal_checkpoint(TRUNCATE),再复制主文件。问题是只要应用还在写,checkpoint 结束到复制之间又可能出现新事务。长读事务也会影响推进;“命令返回了”不等于日志已全部处理。

想观察进度,可在合适的可写维护连接执行 PRAGMA wal_checkpoint(PASSIVE)。它会尝试做 checkpoint,不是纯只读探针;返回的三列需要一起看,不能只看第一列为 0 就宣布全部完成。日常备份直接使用 Backup API,没必要为生成快照先强制清空 WAL。若选择离线复制,应真正停掉所有使用者、正常关闭连接并确认静止状态。

五、备份能打开,还要检查业务数据

integrity_check 检查结构等问题,但不负责检查外键,所以示例另加 foreign_key_check。两项通过也只能说明这两类检查通过,不能证明某笔订单、某条配置或某段流水一定存在。

恢复时先把备份放到隔离目录,由测试实例读取,禁止直接覆盖生产库。核对关键表数量、业务唯一编号、抽样记录、最新业务时间与应用启动结果。在线源库可能继续变化,不能拿稍后的生产总行数机械对比;应记录可追踪的业务标记和备份窗口,按允许的数据恢复点判断。

六、按现象走分支,别靠重试碰运气

现象优先检查处理方向
能打开却缺最近记录是否仅复制主文件改用在线备份并做业务核对
unable to open database绝对路径、挂载、目录权限修正路径与最小权限
database is locked 或持续超时长事务、写入压力、备份耗时有限重试,必要时选维护窗口
disk is full目标空间与文件系统状态停止归档该副本,处理容量
检查报错或外键有结果失败副本及原始错误保留证据,不宣称备份成功

WAL 支持读写并行,但同时仍只有一个写者。先理顺事务边界和备份方式,再考虑迁移数据库。

七、上线前验收清单

  • 源路径已核实,错误路径不会自动创建空库。
  • 目标与源严格分开,备份失败有告警,不覆盖最后一份可用副本。
  • 记录开始时间、结束时间、运行库版本与退出状态。
  • 完整性检查、外键检查以及业务抽样分别通过。
  • 隔离恢复可以启动应用,满足预定恢复点与恢复时间要求。
  • 生产连接活跃时不手删 WAL/SHM,不靠降低持久性换取表面成功。

下文示例的机制已在独立临时测试库验证;没有对任何生产 SQLite 数据库执行复制、checkpoint 或恢复。实验说明方法可行,不代表所有负载都能在同一耗时内完成。

八、官方参考与适用边界

本文是长期有效的故障排查,不是新功能报道。运行库应采用仍受维护且包含已知修复的版本;Python 版本不等于其链接的 SQLite 版本,以 sqlite3.sqlite_version 为准。

🔕 评论已关闭