春江暮客

春江暮客的个人学习分享网站

用 Python 备份 SQLite 数据库:WAL 快照与独立恢复验证

2026-10-13 技术
用 Python 备份 SQLite 数据库:WAL 快照与独立恢复验证

批处理把进度保存在 SQLite 中,想备份时直接复制了 .sqlite 文件,恢复后却发现最近提交的记录不见了。WAL 模式下,新数据可能还在 -wal 文件里。本文用 Python 的备份 API 生成独立快照,再把它复制到全新目录查询,确认备份真的能读取。

适合初学者到中级读者,承接SQLite 批处理断点恢复。演示不接触真实数据库,也不需要第三方 Python 包。

1. 方法:通过 SQLite 连接备份

SQLite WAL 文档说明,WAL 文件属于数据库的持久状态;只复制主文件可能遗漏已提交数据。运行中的数据库也不适合逐个复制主文件和旁边的日志文件,因为复制期间状态可能变化。

使用 Python Connection.backup(),由 SQLite 读取数据库状态。本文只备份 main 数据库,不处理其他 ATTACH 数据库或外部结果文件。准备一个可信、可写的本地目录,文件系统需要支持硬链接。

流程是:备份到同目录下的临时目录 → 切换备份的日志模式 → 检查结构 → 关闭连接 → 创建最终文件名。源连接用 mode=ro 打开,不执行写入 SQL;目标已存在时拒绝覆盖。

2. 准备环境

需要 Python 3.10 或更新版本,且包含标准库 sqlite3。本文在 Linux、Python 3.11.4 上验证。先在新的工作目录下载 backup_sqlite.py、demo_wal.py 和 verify_backup.py,然后执行:

python3 --version
python3 -c 'import sqlite3; print(sqlite3.sqlite_version)'
python3 backup_sqlite.py --help

第二条命令显示 Python 实际链接的 SQLite 版本。并发 WAL 应用应使用包含官方修复的构建;SQLite 的 WAL-reset 修复说明列出 3.51.3、3.50.7、3.44.6 等修复版本,发行版也可能回移补丁。本文演示不并发执行写入和检查点。

3. 完整备份脚本

backup_sqlite.py 的内容如下:

"""Back up one SQLite database without overwriting a destination. Python 3.10+."""
import argparse
from contextlib import closing
import os
from pathlib import Path
import sqlite3
import sys
import tempfile


def backup_database(source, destination):
    source = Path(source).resolve(strict=True)
    destination = Path(destination).absolute()
    if not source.is_file():
        raise ValueError("source must be a regular file")
    if os.path.lexists(destination):
        raise FileExistsError(f"destination already exists: {destination}")
    if not destination.parent.is_dir():
        raise ValueError("destination parent directory must exist")

    # A sibling temporary directory keeps the hard link on one filesystem.
    with tempfile.TemporaryDirectory(prefix=".sqlite-backup-", dir=destination.parent) as work:
        temporary = Path(work) / "backup.sqlite"
        with closing(sqlite3.connect(source.as_uri() + "?mode=ro", uri=True,
                                     isolation_level=None)) as src:
            with closing(sqlite3.connect(temporary, isolation_level=None)) as dst:
                src.backup(dst, pages=256, sleep=0.1)
                # Make the result a standalone file even when the source uses WAL.
                mode = dst.execute("PRAGMA journal_mode=DELETE").fetchone()[0]
                if mode.lower() != "delete":
                    raise ValueError("could not select DELETE journal mode")
                checks = dst.execute("PRAGMA quick_check").fetchall()
                if checks != [("ok",)]:
                    raise ValueError(f"quick_check failed: {checks}")
        os.chmod(temporary, 0o600)
        # Unlike replace(), link() fails if another process created the destination.
        os.link(temporary, destination)
    return destination


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("source", type=Path)
    parser.add_argument("destination", type=Path)
    args = parser.parse_args()
    try:
        destination = backup_database(args.source, args.destination)
    except (OSError, ValueError, sqlite3.Error) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    except KeyboardInterrupt:
        print("INTERRUPTED", file=sys.stderr)
        return 130
    print(f"BACKUP OK: {destination}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

resolve(strict=True) 和只读 URI 防止把拼错的源路径当作新数据库。isolation_level=None 让这里的连接不自动开启隐式写事务,也兼容 Python 3.10/3.11。

os.link() 为完整临时文件创建最终名称;如果另一进程抢先创建了目标,它会失败而不会覆盖。临时目录和目标在同一文件系统,结束后临时名称被清理。最终备份权限为 0600,Linux 上仅文件所有者可读写。该流程没有做断电持久性测试。

4. 构造有真实 WAL 数据的示例

将下面代码保存为 demo_wal.py。它保留源连接,提交两条记录后启动备份子进程,备份完成才向源库加入第三条记录。

"""Keep committed rows in WAL while a separate backup process runs."""
from pathlib import Path
import sqlite3
import subprocess
import sys

folder = Path("sqlite-backup-demo")
folder.mkdir(exist_ok=True)
source = folder / "live.sqlite"
if source.exists():
    raise SystemExit("Use a fresh demo directory")
con = sqlite3.connect(source, isolation_level=None)
try:
    assert con.execute("PRAGMA journal_mode=WAL").fetchone()[0] == "wal"
    con.execute("PRAGMA wal_autocheckpoint=0")
    con.execute("CREATE TABLE jobs (id INTEGER PRIMARY KEY, status TEXT NOT NULL)")
    con.executemany("INSERT INTO jobs VALUES (?, ?)", [(1, "done"), (2, "done")])
    assert Path(str(source) + "-wal").stat().st_size > 0
    print("WAL has committed data", flush=True)
    subprocess.run([sys.executable, "backup_sqlite.py", str(source),
                    str(folder / "snapshot.sqlite")], check=True)
    con.execute("INSERT INTO jobs VALUES (?, ?)", (3, "later"))
    print("LIVE rows=3; snapshot should still have 2", flush=True)
finally:
    con.close()

wal_autocheckpoint=0 只用于演示,确保自动检查点不会提前清空这个小样本的 WAL。不要照搬到生产任务;生产环境需要自己的检查点策略。

5. 验证独立恢复和查询结果

将以下内容保存为 verify_backup.py。它只复制备份文件到新的临时目录,那里没有源库的 -wal 或 -shm 文件。

"""Read the backup in a fresh directory without the source or its WAL files."""
from contextlib import closing
from pathlib import Path
import shutil
import sqlite3
import tempfile

backup = Path("sqlite-backup-demo/snapshot.sqlite").resolve(strict=True)
with tempfile.TemporaryDirectory(prefix="sqlite-restore-") as work:
    restored = Path(work) / "restored.sqlite"
    shutil.copyfile(backup, restored)
    with closing(sqlite3.connect(restored.as_uri() + "?mode=ro", uri=True,
                                 isolation_level=None)) as con:
        assert con.execute("PRAGMA quick_check").fetchall() == [("ok",)]
        assert con.execute("PRAGMA journal_mode").fetchone()[0] == "delete"
        rows = con.execute("SELECT id, status FROM jobs ORDER BY id").fetchall()
        assert rows == [(1, "done"), (2, "done")], rows
print("RESTORE OK: [(1, 'done'), (2, 'done')]")

运行:

python3 demo_wal.py
python3 verify_backup.py

预期输出如下,绝对路径取决于工作目录:

WAL has committed data
BACKUP OK: /your/workdir/sqlite-backup-demo/snapshot.sqlite
LIVE rows=3; snapshot should still have 2
RESTORE OK: [(1, 'done'), (2, 'done')]

备份中应只有 ID 1、2,后写入源库的 ID 3 不会自动进入旧快照。PRAGMA quick_check用于快速结构检查,不检查所有索引一致性,也不验证业务正确性。需要更全面的结构检查时可用 integrity_check;实际恢复还要核对关键记录、行数和业务约束。

6. 验证拒绝覆盖,再用于自己的任务

  1. 对同一路径再次备份,确认非零退出码,同时旧备份仍可读取:
python3 backup_sqlite.py sqlite-backup-demo/live.sqlite sqlite-backup-demo/snapshot.sqlite
printf 'exit=%s\n' "$?"
python3 verify_backup.py

应看到 destination already exists、exit=1 和 RESTORE OK。不要用删除旧备份来绕过这个检查;选择新名称。

  1. 用时间戳生成新名称:
mkdir -p backups
python3 backup_sqlite.py sqlite-backup-demo/live.sqlite "backups/progress-$(date +%Y%m%d-%H%M%S).sqlite"

这是对演示源库生成另一个快照,其中已经有第三条记录;上面的两行断言只用于第一个演示快照。换成自己的数据库后,应修改验证查询。重复运行到同一秒仍可能发生名称冲突,此时脚本安全退出。

定时运行时,可参考固定 Python 路径和工作目录,使用绝对路径并检查退出码。备份保留数量、异地复制和恢复演练需要另行安排;同一块磁盘上的副本不能覆盖磁盘损坏的场景。

7. 常见错误与处理

现象 具体处理
源文件不存在 用 ls -l /实际路径/progress.sqlite 核对名称,脚本不会创建空源库。
destination parent directory must exist 先运行 mkdir -p backups,再传入该目录中的新文件名。
Permission denied 用 ls -ld backups 核对运行用户和目录权限;只读 WAL 连接也需要合适的旁文件或目录权限,见 SQLite 只读 WAL 条件。
Operation not supported 换用支持硬链接的本地备份目录;脚本失败时不会发布最终文件名。
database is locked 或长时间不结束 避开长期事务或密集写入时段,先在演示库验证;sleep=0.1 是重试间隔,不是总超时。
No space left on device 用 df -h backups 检查空间,按磁盘满排查教程恢复可用空间,再生成新备份。

SQLite 备份 API 说明指出,频繁写入可能使备份反复重启。大库应安排合适的运行窗口;本例不承诺从调用开始那一刻截取快照,也不设置整个备份过程的截止时间。

8. 小结

通过 SQLite 连接生成备份,检查完成后再发布新文件名,可以避免误覆盖已有副本。先用有 WAL 数据的演示库验证,再把独立恢复查询换成自己的关键业务检查。保留旧快照并定期演练恢复,备份才有实际用途。

标签

1024 12306 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena copy cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization debugging deployment desktop-app devtools disk-space disown docker dovecot download electron esm esm2 esm3 esmc esmfold2 faceswap fasta fastmcp faulthandler ffmpeg file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo indexnow javascript jev json just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pdb pep-723 phaser pillow pip postfix preprocessing print protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 signals sklearn solana somaticsignatures spl sqlite ssh standardize static-site subprocess sysadmin system-one tensorflow tkinter toml tron tronpy troubleshooting turtle typesafe-ai ubuntu usdt uv vhhbert vite webp wordcloud wordpress workflow yaml 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它