把 SQLite 跑上生产:四个参数和一个备份底线

作者:程序员白大力 · 2026-09-28 09:00
把 SQLite 跑上生产:四个参数和一个备份底线

封面

小项目上线的剧本通常有两种:一种是被「生产必须上 Postgres」的说法说服,在还没遇到任何问题时,先背上数据库服务的安装、监控、调优成本;另一种是什么都不配直接上,流量一来,日志里开始刷 database is locked。

SQLite 的真实定位其实在两者之间:单机、读多写少的应用,配好几个参数就能稳定运行。它的问题从来不是「不能用」,而是默认配置按 1990 年代的单机嵌入式场景设计,直接搬上 Web 生产环境必然水土不服。这篇文章把上生产前真正要做的几件事理清楚。

一、先划边界:它适合什么、不适合什么

参数调得再好,也救不了选错场景。SQLite 的适用边界很清晰:

维度 适合 不适合
部署形态 单台服务器,应用和库文件同机 多台机器共享同一个库文件
负载特征 读多写少:内容站、博客、内部工具 每秒成百上千笔并发写入
运维需求 备份靠复制文件、无需主从复制 需要实时复制、多活
存储 本地磁盘 网络文件系统

最后一行是硬红线:WAL 模式依赖共享内存文件做跨进程协调,NFS 上的锁机制实现不可靠,官方文档明确警告可能导致数据库损坏。VPS 和独立服务器不用操心,但部分共享主机的用户目录 quietly 挂在 NFS 上,上库文件前务必确认存储类型。

至于「多大规模该换 Postgres」,判断依据是并发写入:当多个进程持续排队写、timeout 调到几十秒仍报锁,或者业务需要跨机复制时,才是迁移的真正信号。读多写少的内容站,大概率一辈子用不到。

二、三个参数,解决九成锁报错

默认配置的锁问题出在三处,逐一对应:

默认值 问题 改法
journal 模式 DELETE 写入时读全部阻塞,读写互斥 开 WAL,读写并行
事务模式 DEFERRED 事务先当读者、写到一半升级写锁,升级瞬间被抢直接报错,连 timeout 都不等 改 IMMEDIATE,开事务就拿写锁
busy_timeout 为 0 锁被占立刻报错,不等待 设 timeout,锁冲突时排队重试

WAL(预写日志)的原理不复杂:写操作先追加进独立的 -wal 文件再合并回主库,读者读主库加日志,于是读不再阻塞写、写不再阻塞读,代价是同一时刻仍只有一个写者。IMMEDIATE 则是把「先读后升级写锁」的竞态直接消掉:有开发者做过对比压测,只加这一个参数,错误率从每秒几十笔掉到接近零。

Django 5.1 起这三个参数都能在配置文件里直接声明,下面是最小可用版本:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
        "OPTIONS": {
            "timeout": 20,                     # 锁等待上限(秒)
            "transaction_mode": "IMMEDIATE",   # 开事务即取写锁
            "init_command": (
                "PRAGMA journal_mode=WAL;"
                "PRAGMA synchronous=NORMAL;"
                "PRAGMA journal_size_limit=67108864;"
            ),
        },
    },
}

为什么这么写:WAL 是持久设置,写一次就记录进库文件;其余 PRAGMA 是每个连接各自生效,所以放进 init_command,每次新建连接自动执行,谁也不会漏。journal_size_limit 把 WAL 文件封顶在 64MB,防止写入高峰时它无限膨胀。旧版 Django 没有这两个选项,需要自定义数据库引擎或用连接信号执行 PRAGMA——与其修补,不如先升级。

一个容易忽略的连带影响:如果项目开了 ATOMIC_REQUESTS(每个请求包一个事务),配上 IMMEDIATE 之后连纯读页面都要先拿写锁,全站请求开始排队,读多写少的站反而变慢。更好的做法是关掉它,只把真正需要事务的写操作包进 transaction.atomic()。

三、synchronous=NORMAL 的安全账

落盘策略 synchronous 是唯一需要算一笔安全账的参数:

■
FULL(默认):每次提交都强制刷盘,最安全,也最慢;
■
NORMAL:官方在 WAL 场景下的推荐档——应用崩溃不丢已提交数据;只有整机掉电,才可能丢掉最近几笔还没合并回主库的提交,且库文件本身不会损坏;
■
OFF:速度最快,掉电可能损坏库文件,生产环境别碰。

关键纪律是配对:NORMAL 的安全性建立在 WAL 之上。如果不开 WAL 只设 NORMAL,掉电时损坏的是真正的风险。对内容站来说这笔交易几乎无感——丢几秒的最新评论可以接受,库文件损坏才是事故。

四、备份底线:别用 cp

参数配好只保证了「不坏」,备份才决定「坏了能不能救」。最常见的错误做法恰恰是最直觉的:写个 cron 对着 .db 文件跑 cp。应用正在写入时复制文件,拷出来的备份可能是事务上不一致的坏文件,平时看不出来,恢复那天才发现是废纸。

SQLite 内置了两个安全备份命令,都不用停服:

# 单事务快照,比 .backup 更适合写入频繁的库
sqlite3 /path/to/db "VACUUM INTO '/path/backup.db'"
gzip /path/backup.db

VACUUM INTO 在一个事务内拍快照,顺带整理碎片、缩小体积;.backup 命令效果等价。之后把压缩文件按滚动命名(如按日期)传到对象存储,异地备份就有了。

要求恢复点更小的场景(比如用户数据频繁变更),可以上 Litestream:它作为常驻进程盯着 WAL 文件,把每次写入增量流式复制到 S3 兼容存储,恢复点从「昨晚」缩短到「几秒前」,一条 litestream restore 命令即可回滚。

最后一条底线:没验证过恢复的备份不叫备份。定期下载一份备份,跑 PRAGMA integrity_check,再真的把应用指向它启动一次——测试的成本几分钟,赌注是整个数据库。

踩坑与教训
■
-wal 和 -shm 是库的一部分:伴生文件跟着库文件走,备份脚本别单独碰它们,应用开着时手动删除等于制造损坏。
■
大批量删除要分批:一口气 DELETE 几十万行,写锁占用超过 timeout,其他写请求全部跟着超时。用 LIMIT 循环分批删,每笔事务快速结束。
■
迁移也会抢锁:SQLite 部分改表操作是「建新表、拷数据、换名字」,重建大表时短暂独占写库——timeout 给足,在低峰期执行,一般都能平稳度过。
结尾

SQLite 从来不是「不能上生产」,而是「默认配置不能上生产」。划清场景边界、配好 WAL 与事务模式、算清落盘的安全账、守住不用 cp 备份的底线,这四个参数加一条纪律,就够一个小项目安安稳稳跑上很久。省下来的数据库运维时间,拿去做真正有价值的事。

如果这篇文章帮你省了一次多余的 Postgres 部署,欢迎转发给正在纠结的同事。

参考文章
■
Django 文档:Databases(SQLite notes)
■
SQLite 官方文档:Write-Ahead Logging
■
SQLite 官方文档:PRAGMA Reference
■
Litestream 官方文档:Cron-based backup
■
Django SQLite Production Config(Anže Pečar)
■
Optimizing SQLite for Django in Production(Isaac Bythewood)

#Django #SQLite #后端开发 #数据库 #Web开发 #服务器运维

本文由 AI 辅助生成并经人工审核发布,内容仅供参考,不构成法律意见。