SQLite 开启 WAL 模式:小项目并发锁问题的一次彻底解决
一、问题是怎么出现的
单机部署的轻量服务里,SQLite 的性价比几乎无可替代:零运维、单文件、备份即复制。但很多人在并发稍微上来一点之后会撞上同一堆墙——database is locked。
我维护的一个 Flask + SQLite 内部库存系统就踩过这个坑:日常只有十几个人用,读请求占九成,写入集中在上午出库时段。在默认的 rollback journal 模式下,写操作要独占整个数据库文件,读操作也得等写提交完成。结果就是两个人同时点「出库」,后一个直接报锁超时。
二、WAL 到底改变了什么
WAL(Write-Ahead Logging)的核心变化是:把修改先追加到独立的 -wal 文件,提交时只写日志、不覆盖主库。直接效果有三个。
读不再被写阻塞:读操作看主库加上已提交的 WAL 页快照。写不再被读阻塞:写只往日志尾部追加。只有「写 vs 写」仍然串行。
也就是说,读多写少的业务(绝大多数后台管理系统都属于这类)能拿到接近线性的并发读能力,而写入串行本来就是 SQLite 的设计前提,不会更差。
三、三行 PRAGMA 的取舍
PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000;
第一条切换日志模式,是持久化设置,写进数据库头,不用每次连接都执行。
第二条把 fsync 从「每次提交」降为「检查点才 fsync」。这是 WAL 下性价比最高的一步:断电最多丢最近若干个已提交事务,但数据库本身不会损坏。如果业务不能接受丢事务,就保留 FULL。
第三条才是很多人忽略的关键。busy_timeout 让连接遇到锁时自动等待指定毫秒再报错,而不是立刻抛异常。把它设成 5 秒之后,我这边「两人同时出库」的报错率从每天几次降到零。
四、三个容易翻车的细节
一是 -wal 和 -shm 是数据库的一部分。备份时只拷 .db 会丢数据,要用 sqlite3 .backup 或 VACUUM INTO,不要直接 cp。
二是不要把库放在 NFS / SMB 共享盘上。WAL 依赖共享内存文件做锁协调,网络文件系统上的锁语义不可靠,会出现难以复现的损坏。
三是 WAL 文件会随写入增长,需要定期 checkpoint。默认 1000 页自动触发,长事务或大批量写入后建议手动跑一次 wal_checkpoint(TRUNCATE),我把它挂在每天凌晨的定时任务里。
五、实测对比
同一台 2 核 4G 的机器,50 并发读加 5 并发写,持续 60 秒:读 P95 延迟从 218 毫秒降到 12 毫秒,写失败率从 3.7% 降到 0,代价是常态多出约 2 MB 的 -wal 文件。数据不算惊艳,但对十几人用的后台系统来说,改动成本只有三行 PRAGMA,收益是彻底消失的锁报错。
六、小结
SQLite 不需要「升级成 PostgreSQL」来解决并发问题,多数时候只是没有打开它本该打开的开关。WAL 模式加上合理的 busy_timeout,再配上正确的备份方式,足够撑起绝大多数单机业务。真正需要换库的信号不是「出现锁」,而是「写入成为瓶颈」——那时候才轮到客户端-服务器架构出场。
- 点赞
- 收藏
- 关注作者
评论(0)