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 的语义看一遍再上生产。

相关推荐
huanmieyaoseng10032 小时前
Mybatis常见面试题
java·开发语言·mybatis
xiaoye-duck2 小时前
【C++:C++11】核心进阶:C++11引用折叠、完美转发与可变参数模板实战详解
开发语言·c++·c++11
csbysj20202 小时前
jEasyUI 创建分割按钮
开发语言
wjs20242 小时前
前端控制器模式(Front Controller Pattern)
开发语言
雾岛听蓝3 小时前
Qt开发核心笔记:从HelloWorld到对象树内存管理与坐标体系详解
开发语言·经验分享·笔记·qt
無限進步D7 小时前
Java 运行原理
java·开发语言·入门
是苏浙7 小时前
JDK17新增特性
java·开发语言
不光头强7 小时前
spring cloud知识总结
后端·spring·spring cloud
GetcharZp10 小时前
告别 Python 依赖!用 LangChainGo 打造高性能大模型应用,Go 程序员必看!
后端