SQLite 能跑生产吗?能。但不是你装个 sqlite3 就开始 SELECT 那种跑法。

大部分人说 SQLite 不能用在生产,其实是在说:我用过默认配置,然后被坑了。这就像买了一台手动挡跑车,一直挂一档开,然后说这车不行。

SQLite 不是 MySQL 的平替,它在另一个维度上——零运维、单文件、嵌入式。选它的场景,图的不是"功能强大",而是"你不需要一个DBA"。

但选它不意味着你没有运维责任。SQLite 在生产上的坑,比大部分人多一层:它不是 C/S 架构,你的脆弱面不在网络上,全在文件系统上。

这篇把 SQLite 在生产上最要命的几个点一个个拆清楚——备份怎么做、并发能撑多大、死锁怎么避免、Docker 里怎么跑、什么时候该换 PostgreSQL。


一、备份:最基础的保命功课

SQLite 备份最大的问题是:它不是一个独立进程,你没法在外部做一致性快照。 你 cp 一个正在被写入的 .db 文件,得到的很大概率是一个损坏的备份。

方案一:.backup 命令(最稳妥)

sqlite3 CLI 自带:

sqlite3 /path/to/prod.db ".backup /path/to/backup.db"

这个命令在 SQLite 内部执行,它知道当前有没有活跃事务、WAL 里有哪些未合并数据。它会等当前事务完成再开始备份,确保副本是一致的。

自动化脚本:

#!/bin/bash
DB=/data/app.db
BACKUP_DIR=/backups/sqlite
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p $BACKUP_DIR
sqlite3 $DB ".backup $BACKUP_DIR/app_$DATE.db"
gzip $BACKUP_DIR/app_$DATE.db
find $BACKUP_DIR -name "*.gz" -mtime +30 -delete

方案二:VACUUM INTO(SQLite 3.27+)

sqlite3 /path/to/prod.db "VACUUM INTO '/backups/app_$(date +%Y%m%d).db'"

.backup 的现代版本。同样保证一致性,而且输出的文件是优化过的(重新整理页面、回收空洞)。但磁盘空间要两倍——它创建一份压缩后的完整副本。

方案三:Litestream(流式备份)

适合需要实时增量备份到 S3 的对象存储的场景。

litestream replicate /data/app.db s3://my-bucket/sqlite/

Litestream 通过监听 SQLite 的 WAL 文件变化,把增量日志实时流到远端。崩溃时可以恢复到任意时间点。对小型生产应用来说,这比任何手动备份脚本都靠谱。

代价: 多一个进程,多一层复杂度。Litestream 本身如果挂了,你会失去实时增量(但已有全量备份不受影响)。

方案四:wal_checkpoint + 文件快照(仅限文件系统支持快照的场景)

如果数据库文件在 ZFS、btrfs、LVM 快照或云盘快照上:

PRAGMA wal_checkpoint(TRUNCATE);
# 然后在文件系统层拍快照

触发 checkpoint 把 WAL 合并回主文件,再拍快照。这样快照里拿到的是一个完整的主文件,不需要额外恢复步骤。


二、WAL 模式并发:你最多能撑多少人?

先给数字,再讲道理。

场景并发阈值说明
纯读几十到几百WAL 下读不阻塞读,SQLite 本身很快
读写混合(写少)几十(50-100)读不受写阻塞,但写串行
读写混合(写多)几到十几写之争锁开始明显
纯写(大量 INSERT)1写事务串行执行

WAL 模式下,SQLite 的核心瓶颈只有一个:写串行。 同时间只有一个写事务能提交。这不是锁粒度的问题——SQLite 就是单线程写引擎。

但对你来说,需要多高的并发取决于你的业务。一个 B 端管理后台——几十个员工在用,SQLite 完全扛得住。一个每小时几百人的电商结算——写操作密集触发锁竞争——你需要想清楚。

怎么测你的并发阈值

# 用 sqlite-bench 或自己写个简单压测
# 模拟 20 个并发连接,每连接随机读写
apg -n 20 -c 20 -d /tmp/test.db

如果并发不够怎么办

WAIT模式(PRAGMA busy_timeout): 不报错,排队等锁。

PRAGMA busy_timeout = 5000;  -- 等5秒,超时再报错

WAL 模式 + 连接池上限控制: 不是给了 100 个连接就有 100 个有效并发。SQLite 的最佳实践是:连接池大小控制在 10-20 之间,多了反而因锁争用导致更差的结果。

分库: 如果写压力大到一个 .db 扛不住,说明场景不适合 SQLite,该换 PostgreSQL 了。


三、PRAGMA 调优清单:生产环境必须改的配置

这些不是"可优化"——是不改就会出问题

-- 1. WAL 模式(必须)
PRAGMA journal_mode = WAL;

-- 2. 同步模式(看风险接受度)
PRAGMA synchronous = NORMAL;
-- FULL = 最安全,写最慢(每次提交都 fsync)
-- NORMAL = 安全性足够,性能好(WAL 模式下 crash 最多丢一页 WAL)
-- OFF = 数据库可能随时损坏,永远不要用

-- 3. 缓存大小(内存够就设大一点)
PRAGMA cache_size = -64000;  -- 64MB,负数表示 KB

-- 4. 临时存储(放内存,别写盘)
PRAGMA temp_store = MEMORY;

-- 5. 内存映射 I/O(大量读时有效)
PRAGMA mmap_size = 268435456;  -- 256MB

-- 6. 外键约束(默认关闭,需要就开)
PRAGMA foreign_keys = ON;

-- 7. 忙着就等着
PRAGMA busy_timeout = 5000;

-- 8. WAL 自动 checkpoint 阈值
PRAGMA wal_autocheckpoint = 1000;  -- WAL 到 1000 页时自动 checkpoint

把这些放到数据库连接初始化里,每次连接都执行一次。

一个 bug 警告

PRAGMA journal_mode 必须在没有任何活跃事务时设置。如果在事务中执行,它静默失败,不报错,但也不生效。很多人以为开了 WAL,实际上一直在 delete 模式。

验证方式:

PRAGMA journal_mode;
-- 应该返回 'wal',不是 'delete'

四、数据一致性和 crash 安全

SQLite 是 ACID 的,前提是你没做任何奇怪的操作。

原子提交

SQLite 的默认行为是每个 SQL 语句独立一个事务。如果你需要多语句原子性:

BEGIN IMMEDIATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

BEGIN IMMEDIATEBEGIN EXCLUSIVE 的区别:

模式行为使用场景
BEGIN DEFERRED等到第一个写操作才拿锁你能承受"拿到锁时失败"的风险
BEGIN IMMEDIATE立即拿写锁,失败就报错你确定接下来要写
BEGIN EXCLUSIVE排它锁,谁都不让进你需要绝对独占访问

建议:如果确定要写,用 BEGIN IMMEDIATE 避免 DEFERRED 模式下,多个连接同时 START TRANSACTION 然后一起试图升级到写锁——这种情况触发文件锁失败,要依赖 busy_timeout 重试。

crash 安全

WAL 模式 + synchronous = NORMAL 的 crash 安全保证:

  • 已提交的事务不会丢
  • 最多丢失一页 WAL(即最多一个事务的中间状态,不会导致文件级损坏)
  • 重启后 SQLite 自动从 WAL 恢复

但如果操作系统崩溃时 WAL 文件本身也被损坏——比 SQLite 自己的数据文件先坏——这种情况极其罕见,但理论上存在。解决方案就一个:Litestream 或定期 .backup。


五、Docker 里跑 SQLite 的坑

坑很多,一个一个说。

文件系统:OverlayFS 的 WAL 问题

WAL 模式 + Docker OverlayFS = 定时炸弹。

Docker 默认的存储驱动是 overlay2。WAL 模式依赖共享文件系统的字节范围锁(POSIX advisory locks)来实现并发控制。OverlayFS 对这些锁的支持历史上坑很多——WAL 文件可能在多个层之间不同步。

标准做法:

  1. 把 .db 文件挂载到宿主机目录(bind mount),不要放在容器层里
  2. 如果跑多个副本(例如 k8s Pod 多实例),WAL 模式冲突几乎必然发生——不要在多副本场景下用 SQLite
volumes:
  - /data/sqlite:/data/sqlite  # bind mount,不走 overlay

文件系统:NFS 根本不能用

NFS 对文件锁的支持一塌糊涂。在 NFS 上跑 SQLite 等于定时自杀。如果多个应用实例共享同一个存储,用 PostgreSQL,别用 SQLite。

内存占用

容器限制内存会导致 WAL 模式下 checkpoint 时 OOM。因为 checkpoint 需要把 WAL 内容合并回主文件,这个过程如果内存不够,直接 OOM kill。

留够余量: cache_size 设置多少,加上 checkpoint 的临时峰值,不要卡得太死。

优雅关闭

容器终止时要确保 SQLite checkpoint 跑完:

PRAGMA wal_checkpoint(TRUNCATE);

启动脚本里加上 trap 捕捉 SIGTERM,触发 checkpoint。否则 WAL 文件里的未合并数据在下次启动时要恢复,恢复本身没问题,但恢复时间取决于 WAL 大小。WAL 太大时启动变慢。


六、SQLite vs PostgreSQL:什么时候换

这是关键问题。很多人不应该用 SQLite,也有很多人不应该上 PostgreSQL。

SQLite 适合的场景

  • 单机服务、单实例部署
  • 并发连接数 <100(实际同时写 <20)
  • 不需要并发写入同一行(热点行竞争)
  • 数据量 < 100GB(再大也能跑,但管理和恢复都不方便)
  • 不需要细粒度权限控制
  • 能接受分钟级恢复时间

典型例子: B 端后台、个人 SaaS 工具、物联网边缘节点、嵌入式设备、本地数据处理管道、开发测试环境。

该换 PostgreSQL 的信号

任何一个成立,就该换:

  1. 写并发持续 >20/s——SQLite 写串行,20 TPS 差不多是它在普通硬件上的上限
  2. 多个实例同时读同一个库——分实例部署但共享数据库,别用 SQLite
  3. 数据量超过 100GB——备份恢复时间开始变长,.backup 耗时你可能接受不了
  4. 需要在线迁移、不停机升级——SQLite 的 schema change 大部分情况要独占写锁
  5. 需要行级细粒度权限——SQLite 只在文件级有权限控制
  6. 团队里有人坚持要用 PostgreSQL——这个理由比上面五个加起来都重要

决策树

你的服务需要数据库吗?
├── 是,但只是临时/单机 → SQLite(零配置起步)
└── 是,生产长期运行 → 
    ├── 单实例 + 写并发低 + 数据<100GB → SQLite
    ├── 多实例/高并发/大数据 → PostgreSQL
    └── 中间地带 → SQLite 起步,设计时预留迁移接口

中间地带策略: 用 SQLite 起步,但所有数据库操作走抽象层(repository pattern)。上线后发现撑不住了,再换 PostgreSQL。SQLite 和 PostgreSQL 在 SQL 语法上 95% 兼容,迁移代价比 MySQL ↔ PostgreSQL 小得多。


总结

维度结论
备份.backup / VACUUM INTO 日常用,Litestream 做实时增量
并发写串行是硬上限,<20 并发写是舒适区
PRAGMAjournal_mode=WAL, synchronous=NORMAL, busy_timeout=5000 是底线
crash安全ACID 保证,但需要 checkpoint 配合
Dockerbind mount 别用 overlay,别跑多副本
换库信号写 >20/s,数据 >100GB,多实例共享,需要不停机迁移

SQLite 不是玩具。它被低估的根本原因不是它不够强,是大部分人在用它的第一天就踩进了默认配置的坑,然后再也没有回头。

你说 SQLite 不行?先告诉我你 WAL 开了没有。


SQLite in Production: Backup, Concurrency, Disaster Recovery, Performance Limits

Can SQLite run in production? Yes. But not the way you install sqlite3 and start SELECTing.

Most people say SQLite isn't production-ready because they used the default configuration and got burned. That's like driving a manual sports car in first gear and concluding the car is junk.

SQLite isn't a MySQL replacement. It lives in a different dimension — zero-ops, single-file, embedded. You don't choose it because it's "powerful." You choose it because you don't want to hire a DBA.

But that doesn't mean no operational responsibility. SQLite's pain points in production run deeper than most databases: it's not client-server. Your vulnerability isn't on the network — it's all on the filesystem.

This article breaks down the most critical production concerns one by one — backup, concurrency limits, deadlock avoidance, Docker gotchas, and when to migrate to PostgreSQL.


1. Backup: The Most Basic Life Insurance

SQLite's biggest backup problem: it's not a separate process — you can't take a consistent snapshot from outside. A cp of a .db file being written to is likely a corrupted copy.

Option 1: .backup command (safest)

sqlite3 /path/to/prod.db ".backup /path/to/backup.db"

This runs inside SQLite itself. It knows about active transactions and unmerged WAL data. It waits for the current transaction to complete before starting.

Automation script:

#!/bin/bash
DB=/data/app.db
BACKUP_DIR=/backups/sqlite
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p $BACKUP_DIR
sqlite3 $DB ".backup $BACKUP_DIR/app_$DATE.db"
gzip $BACKUP_DIR/app_$DATE.db
find $BACKUP_DIR -name "*.gz" -mtime +30 -delete

Option 2: VACUUM INTO (SQLite 3.27+)

sqlite3 /path/to/prod.db "VACUUM INTO '/backups/app_$(date +%Y%m%d).db'"

A modern version of .backup. Same consistency guarantee, but the output is optimized (pages reorganized, free space reclaimed). Requires double disk space temporarily.

Option 3: Litestream (streaming backup)

For real-time incremental backups to S3-compatible storage.

litestream replicate /data/app.db s3://my-bucket/sqlite/

Litestream monitors SQLite's WAL file changes and streams incremental logs to the cloud in real time. You can recover to any point in time after a crash.

Cost: One more process, one more layer of complexity. If Litestream itself dies, you lose real-time incrementals (but full backups are unaffected).

Option 4: wal_checkpoint + filesystem snapshot

Only if your filesystem supports snapshots (ZFS, btrfs, LVM, cloud disk):

PRAGMA wal_checkpoint(TRUNCATE);
# Take filesystem snapshot here

Trigger a checkpoint to merge the WAL back into the main file, then snapshot. The snapshot contains a complete, standalone main file.


2. WAL Mode Concurrency: How Many Users Can You Handle?

Numbers first, theory second.

ScenarioConcurrency ThresholdNotes
Read-onlyTens to hundredsWAL: reads don't block reads
Mixed (few writes)50-100Reads don't block writes
Mixed (many writes)5-15Write contention becomes visible
Write-only (bulk INSERT)1Serialized writes

In WAL mode, SQLite has one core bottleneck: serialized writes. Only one write transaction can commit at a time. This isn't a lock granularity issue — SQLite is fundamentally a single-writer engine.

But what you need depends on your business. An internal admin tool with dozens of employees? SQLite handles it fine. An e-commerce settlement system processing hundreds of orders per hour with write contention? You need to think carefully.

If you need more concurrency:

  • PRAGMA busy_timeout: Don't error, just queue and wait.
    PRAGMA busy_timeout = 5000;  -- wait 5 seconds before error
    
  • Connection pool limit: Keep it at 10-20. More connections means more lock contention, not better throughput.
  • Shard: If write pressure overwhelms one .db, the scenario has outgrown SQLite. Move to PostgreSQL.

3. PRAGMA Tuning Checklist: Mandatory Production Configuration

These aren't "optimizations" — you will have problems without them.

PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -64000;       -- 64MB
PRAGMA temp_store = MEMORY;
PRAGMA mmap_size = 268435456;     -- 256MB
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA wal_autocheckpoint = 1000;

Set these at connection initialization, every single time.

One bug to watch: PRAGMA journal_mode silently fails inside an active transaction. It won't error — it just won't work. Verify:

PRAGMA journal_mode;
-- should return 'wal', not 'delete'

4. Data Consistency and Crash Safety

SQLite is ACID — provided you don't do anything weird.

Atomic commits:

BEGIN IMMEDIATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

Use BEGIN IMMEDIATE when you know you're going to write. It avoids the deadlock-prone scenario where multiple connections start BEGIN DEFERRED and then try to upgrade simultaneously.

Crash safety under WAL + synchronous=NORMAL:

  • Committed transactions are never lost
  • At most one WAL page is lost (partial transaction state, not file-level corruption)
  • SQLite auto-recovers from WAL on restart

5. Docker Gotchas

Filesystem: OverlayFS + WAL = time bomb.

Docker default storage driver is overlay2. WAL mode relies on POSIX advisory locks over shared filesystem. OverlayFS support for these locks has historically been problematic.

Rules:

  1. Bind mount the .db file to a host directory — never use container layers
  2. Don't run SQLite in multi-replica scenarios (k8s with multiple Pod instances)
volumes:
  - /data/sqlite:/data/sqlite  # bind mount, not overlay

NFS: Unusable for SQLite. NFS file lock support is a disaster.

Memory limits: Container memory limits can trigger OOM during WAL checkpoint. Leave headroom.

Graceful shutdown: Ensure checkpoint runs before container stops:

PRAGMA wal_checkpoint(TRUNCATE);

6. SQLite vs PostgreSQL: The Decision Tree

SQLite fits:

  • Single-machine, single-instance deployments
  • Concurrent connections < 100 (actual concurrent writers < 20)
  • No need for concurrent writes to the same row
  • Data < 100GB
  • Acceptable recovery time measured in minutes

Signal to switch to PostgreSQL (any one is sufficient):

  1. Write concurrency consistently > 20/s
  2. Multiple instances reading/writing the same database
  3. Data exceeds 100GB
  4. Online migration / zero-downtime schema changes required
  5. Row-level access control needed
  6. Your team insists on PostgreSQL (this outweighs all five above)

Middle-ground strategy: Start with SQLite, design with a repository abstraction layer. If you outgrow it, migrate to PostgreSQL. SQLite and PostgreSQL are about 95% compatible at the SQL syntax level — much cheaper to migrate between than MySQL ↔ PostgreSQL.


Summary

DimensionConclusion
Backup.backup / VACUUM INTO for daily, Litestream for real-time
ConcurrencySerialized writes is the hard cap; < 20 concurrent writers is comfort zone
PRAGMAjournal_mode=WAL, synchronous=NORMAL, busy_timeout=5000 is the baseline
Crash safetyACID guaranteed, but needs checkpoint discipline
DockerBind mount, not overlay; no multi-replica
Migration signalWrites >20/s, data >100GB, multi-instance sharing, zero-downtime needed

SQLite isn't a toy. It's underestimated because most people trip on the default configuration on day one and never come back.

You say SQLite doesn't work in production? Did you turn on WAL first?