SQLite WAL 模式踩坑笔记:高并发读写下的几个细节

最近把一个内部服务的存储从 PostgreSQL 换成 SQLite + WAL 模式,跑了一个多月,遇到几个不太显眼但挺关键的坑,记录一下。

为什么用 WAL

默认的 rollback journal 模式在写事务期间会阻塞所有读,对我们这种"读多写少但读不能停"的场景非常不友好。WAL(Write-Ahead Logging)的好处是:

  • 读和写互不阻塞

  • 多个读事务可以并发执行

  • 写性能在多数场景下更好

开启方式很简单:

```sql

PRAGMA journal_mode=WAL;

```

这个 pragma 是持久的,设一次后数据库文件本身的 header 就改了,下次连接不用重设。

坑 1:`-shm` 和 `-wal` 文件不能漏

WAL 模式下数据库会变成三个文件:

```

mydb.sqlite

mydb.sqlite-shm

mydb.sqlite-wal

```

备份脚本如果只复制主文件,恢复出来的数据会是上次 checkpoint 时的状态,**最近的写会全部丢**。正确做法是先调用 `PRAGMA wal_checkpoint(TRUNCATE);` 把 WAL 落盘清空,再复制主文件。或者直接用 SQLite 的在线备份 API(`sqlite3_backup_init`)。

坑 2:`busy_timeout` 必须显式设

WAL 不是万能的。写事务之间还是互斥的,如果两个进程同时尝试写,后来的会立即拿到 `SQLITE_BUSY`。默认 busy handler 直接返回错误,应用层就要写一堆重试逻辑。更好的做法是:

```sql

PRAGMA busy_timeout=5000;

```

这样 SQLite 会在内部等待最多 5 秒,期间持续重试拿写锁。注意这是**每个连接**都要单独设的,不会从某个全局配置继承。

坑 3:长读事务会让 WAL 无限增长

WAL checkpoint 只能 checkpoint 那些已经被所有 reader 看完的页。如果有一个长读事务一直挂着(比如一个忘记关掉的 cursor),WAL 文件会持续增长,永远 checkpoint 不掉。我们生产环境出现过 WAL 涨到 30GB 的情况。

排查方法是查看 `PRAGMA wal_checkpoint(PASSIVE);` 的返回值,第一个数字是 WAL 中的总页数,第二个是已 checkpoint 的页数,第三个是已 checkpoint 但还没复用的页数。如果第二个长期远小于第一个,就说明有 reader 卡住。

坑 4:`synchronous=NORMAL` vs `FULL`

WAL 模式下默认的 `synchronous=FULL` 仍然会在每次提交都 fsync WAL 文件,性能损失比较大。官方文档明确说:在 WAL 模式下用 `NORMAL` 是安全的,崩溃恢复仍然能保证一致性,只是断电瞬间最后那个事务可能丢失。

```sql

PRAGMA synchronous=NORMAL;

```

我们的测试里这个改动让批量插入吞吐量提升了大概 3 倍。

总结

切到 WAL 之后整体延迟和并发表现都好了很多,但 SQLite 的"省心"是建立在你理解它内部行为的前提下的。建议至少把 `journal_mode`、`busy_timeout`、`synchronous`、`wal_autocheckpoint` 这几个 pragma 的语义看一遍再上生产。

相关推荐
飘尘5 分钟前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
浏览器工程师1 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
行者全栈架构师1 小时前
Maven dependency:tree 的 8 个高级用法
java·后端
Chenyiax1 小时前
从一次请求看懂 OkHttp:架构、调度与连接管理
后端
爱勇宝2 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
AskHarries2 小时前
工具失败时怎么办:重试、回滚、人工确认和风险提示
后端·程序员
苏三说技术4 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎5 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode5 小时前
Redis 在生产项目的使用
前端·后端
用户559822481225 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端