SQLite WAL 模式实战:并发读写、检查点与常见坑
一、背景:默认日志模式为什么容易卡
SQLite 默认使用 rollback journal。写事务开始后,数据库文件会被加锁,其他连接既不能写,也很难稳定地读;只有等事务提交、日志回滚完成,锁才会释放。单机小工具感觉不明显,但一旦放到 Web 服务、采集程序或桌面端多进程场景里,"偶发 database is locked"就会频繁出现。
WAL(Write-Ahead Logging)把修改先追加到 -wal 文件,再在合适的时机回写主库。它的核心收益不是"写入更快",而是读写不再互相阻塞:读连接看到的是某个已提交快照,写连接继续向后追加。对于"读多写少"的业务,这种并发模型通常比单纯调大缓存更直接。
二、开启方式与关键 PRAGMA
开启 WAL 只需要一条语句,但它是持久属性,会写进数据库头,不是每个连接都要重复设置:
PRAGMA journal_mode = WAL;
配套参数建议一起设置:
PRAGMA synchronous = NORMAL; -- WAL 下的常用折中
PRAGMA busy_timeout = 5000; -- 等锁最多 5 秒,避免立刻报错
PRAGMA wal_autocheckpoint = 1000; -- 每约 1000 页自动检查点
PRAGMA foreign_keys = ON;
synchronous=NORMAL 在 WAL 模式下通常足够:断电可能丢失最近若干已提交事务,但数据库不会损坏。如果业务对掉电一致性要求极高,再考虑 FULL,代价是每次提交都要 fsync。
三、并发模型:单写多读,不是多写
WAL 允许一个写事务和多个读事务同时进行,但同一时刻仍然只有一个写者。多个连接同时写,后到者只能等待,或者返回 SQLITE_BUSY。所以:
- 读多写少的 API、报表、缓存服务,收益最明显;
- 高频写入仍要做批量提交或单写队列,不能指望 WAL 把写也并行化;
- 长读事务会阻止检查点回收 WAL,务必控制读事务的生命周期。
四、checkpoint:WAL 文件为什么会越来越大
WAL 不会无限停留在日志里,checkpoint 会把已提交的页写回主库,并在条件允许时截断 WAL。触发方式有三种:自动检查点、显式 PRAGMA wal_checkpoint(...)、以及关闭最后一个连接时的清理。
常见误区是只看主库大小,忽略 -wal 文件。若存在持续的长读事务,checkpoint 无法推进,-wal 会不断膨胀,磁盘占用和恢复时间都会受影响。排查时先看:
PRAGMA wal_checkpoint(PASSIVE);
返回结果里的三个数字分别是"是否被阻塞、日志帧数、已回写帧数"。如果长期被阻塞,优先找未结束的读事务,而不是盲目调大 wal_autocheckpoint。
五、几个容易踩的坑
- 忘记设置 busy_timeout:默认几乎立即返回 BUSY。应用层应该设置合理的等待时间,并给最终失败准备重试与降级。
- 把数据库放在网络文件系统:WAL 依赖共享内存和文件锁语义,NFS、SMB 上的行为并不可靠,生产环境应使用本地盘。
- 备份只拷贝主库:WAL 模式下还有
-wal和-shm。正确做法是用VACUUM INTO或 SQLite 在线备份 API,而不是直接cp。 - 多进程 fork 后共用连接:连接不能跨 fork 共享,子进程应各自打开数据库。
- 只读介质或只读连接:WAL 数据库需要可写的
-shm,纯只读挂载可能无法打开,必要时使用immutable=1或先做快照。 - 长事务里做大量计算:写事务持续时间越长,其他写者等待越久,WAL 也越难回收。
六、一个可用的配置模板
import sqlite3
conn = sqlite3.connect("app.db", timeout=5, isolation_level=None)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
conn.execute("PRAGMA busy_timeout=5000")
conn.execute("PRAGMA wal_autocheckpoint=1000")
conn.execute("PRAGMA foreign_keys=ON")
写操作尽量短,批量插入用显式事务包起来;读操作避免在事务里做网络请求或耗时计算。需要强制收敛 WAL 时,在低峰期执行 PRAGMA wal_checkpoint(TRUNCATE),而不是在高频写入路径上反复调用。
七、监控建议
至少关注三个指标:-wal 文件大小、SQLITE_BUSY 出现次数、checkpoint 是否长期被阻塞。把这三项接入日志或监控后,大多数"数据库突然变慢"都能快速定位。WAL 不是银弹,但它用很小的配置成本换来了更清晰的并发边界,值得作为 SQLite 服务的默认起点。
- 点赞
- 收藏
- 关注作者
评论(0)