SQLite 是链接进应用的数据库引擎,而不是单独运行的数据库服务器。主数据库文件以可移植格式保存模式、表和索引,因此适合本地应用状态、嵌入式设备,以及写入负载符合其并发模型的服务。这个文件依然受事务与锁管理,不能只凭普通文件复制的直觉来操作它,否则容易把方便的存储形式误解成没有数据库运行规则。

坏掉的样子

主文件并不总是当前数据库状态的全部。WAL 模式可以让已经提交的变更仍位于独立 -wal 文件中,而主文件停留在更早的检查点。连接之间的共享内存协调通常还会使用-shm 旁路文件。因此,连接保持活动时只复制主文件,可能漏掉已经提交的数据;依次复制几个正在变化的文件,也不会自动组成一致快照。

WAL 模式改善读写之间的重叠执行,但仍然只允许一个写者。一个长写事务可能让另一个写者收到数据库忙错误。等待更久只是增加耐心,不会增加同时写入者的数量。应缩短事务,并在实际测得的写入并发或远程访问要求超出这种架构的适用范围时,选择有独立服务端的数据库,而不是一味调大超时。

读者也有快照边界。另一个连接持有未提交更新时,读者仍能看见较旧的已提交值。这体现的是事务隔离,并不证明 SQLite 丢失了那次修改。错误之后,应用应检查事务状态,只有操作语义允许时才重试适当范围的事务。不能把所有忙错误、旧值和提交失败都归为同一种可以无限重试的现象。

快速验证

把下面内容保存为 sqlite_lab.py,使用普通 python3 运行并保持断言开启。它只使用标准库和本地临时文件。本次环境是 Python 3.12.14 搭配 SQLite 3.53.1;可以读取模块的引擎版本属性确认,不能用 Python 版本替代实际链接的数据库版本。所有文件都由实验自己创建,不需要连接任何已有数据库。

import shutil
import sqlite3
import tempfile
from contextlib import closing
from pathlib import Path

with tempfile.TemporaryDirectory(prefix="xbcatm-sqlite-") as directory:
    root = Path(directory)
    database = root / "source.db"
    with closing(sqlite3.connect(database, isolation_level=None, timeout=0.05)) as first, \
         closing(sqlite3.connect(database, isolation_level=None, timeout=0.05)) as second:
        assert first.execute("PRAGMA journal_mode=WAL").fetchone() == ("wal",)
        first.execute("PRAGMA wal_autocheckpoint=0")
        second.execute("PRAGMA wal_autocheckpoint=0")
        first.execute("CREATE TABLE marker (id INTEGER PRIMARY KEY, value INTEGER)")
        first.execute("INSERT INTO marker VALUES (1,10)")
        assert first.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone() == (0, 0, 0)
        first.execute("BEGIN IMMEDIATE")
        first.execute("UPDATE marker SET value=99 WHERE id=1")
        assert second.execute("SELECT value FROM marker").fetchone() == (10,)
        try:
            second.execute("BEGIN IMMEDIATE")
        except sqlite3.OperationalError as error:
            assert error.sqlite_errorcode == sqlite3.SQLITE_BUSY
            assert not second.in_transaction
            print("second_writer", error.sqlite_errorname)
        else:
            raise AssertionError("second writer unexpectedly acquired the lock")
        first.execute("ROLLBACK")
        second.execute("BEGIN IMMEDIATE")
        second.execute("UPDATE marker SET value=20 WHERE id=1")
        second.execute("COMMIT")
        assert first.execute("SELECT value FROM marker").fetchone() == (20,)
        # Deliberately unsafe copy, only as a negative control in this quiet fixture.
        shutil.copyfile(database, root / "main-only.db")
        with closing(sqlite3.connect(root / "main-only.db")) as incomplete:
            assert incomplete.execute("PRAGMA integrity_check").fetchone() == ("ok",)
            assert incomplete.execute("SELECT value FROM marker").fetchone() == (10,)
            print("main_only_copy", "integrity ok, stale value 10")
        with closing(sqlite3.connect(root / "backup.db")) as backup:
            first.backup(backup)
            assert backup.execute("PRAGMA integrity_check").fetchone() == ("ok",)
            assert backup.execute("SELECT value FROM marker").fetchone() == (20,)
            first.execute("UPDATE marker SET value=30 WHERE id=1")
            assert backup.execute("SELECT value FROM marker").fetchone() == (20,)
            print("online_backup", "value 20, source now 30")

夹具先建立已经完成检查点的值十,然后开启写事务,把它改成九十九但不提交。另一个连接仍然读到十;它试图成为写者时,必须得到数据库忙错误,并且仍然处于事务之外。脚本随后回滚第一个写者,让第二个连接取得写入位置,再成功提交二十。这同时观察到写入竞争与回滚后的正常推进。

这里关闭自动检查点,只为了让主文件与已提交日志之间的差别保持可见。故意不完整的副本仅包含主文件,它能够通过完整性检查,却读出十。这证明结构完整并不能建立所要求的新鲜度。这个负向对照依赖安静、已知的执行顺序,并不是推荐把活动数据库文件直接复制作为备份办法。

正确副本使用连接的在线备份方法,底层调用 SQLite 备份接口。目标能够通过完整性检查,并且包含二十。随后源库继续变成三十,备份仍然保持二十,说明它是一份已经捕获的一致状态,而不是持续同步的复制节点。实验结束后,所有连接关闭,私有临时目录也随之清理,不留下依赖活动连接才能读取的交付物。

根据访问模式选择引擎

源数据库还在活动时,应采用在线备份接口或其他文档明确支持的备份方法。除了数据库结构,还应核对应用事实,并记录恢复策略需要的捕获状态或备份时间。主文件副本已经证明:一份看起来完全合法的文件,仍然可能对应错误的恢复位置。只检查文件能够打开,无法证明刚刚确认成功的写入也在里面。

WAL 模式所用存储必须满足 SQLite 对锁和共享内存协调的要求。普通WAL 模式期望参与进程位于同一宿主机,共享网络目录不能直接替代数据库协议。由于不存在单独维护数据库登录角色的 SQLite 服务端,访问控制主要依赖文件权限和应用边界。把文件放在哪里、允许哪些进程访问,同样属于数据库部署设计。

在判断引擎是否足够之前,应测量写者占用、忙错误、事务持续时间以及备份行为。数据库体积本身不是写入并发上限。较大的以读取为主的负载可能很适合,而很小但存在大量长写事务竞争的数据库也可能不适合。应根据这些已经观察到的需求选择,而不是因为文件形式简单就认定它不够用,或认定它无需运行约束。

来源