SQLite 的 WAL 模式:为什么开了它读写就不再打架

举报
yd_266547722 发表于 2026/11/10 18:57:46 2026/11/10
【摘要】 上周排查一个线上问题:一个单机服务用 SQLite 存配置和任务队列,平时好好的,一到日志分析任务跑起来,API 接口就集体卡顿几百毫秒。查到最后不是 CPU 也不是连接池,是 SQLite 默认日志模式下的读写互斥。改成 WAL 之后,同样负载下接口的 P99 从 600ms 掉到了 20ms 以

上周排查一个线上问题:一个单机服务用 SQLite 存配置和任务队列,平时好好的,一到日志分析任务跑起来,API 接口就集体卡顿几百毫秒。查到最后不是 CPU 也不是连接池,是 SQLite 默认日志模式下的读写互斥。改成 WAL 之后,同样负载下接口的 P99 从 600ms 掉到了 20ms 以内。

这篇把 WAL 的原理、适用场景和踩过的坑讲清楚。

一、默认的 rollback journal 到底慢在哪

SQLite 默认使用 rollback journal(回滚日志)模式。一次写事务的流程大致是:

  1. 把要修改的页的**原始内容**复制到 `xxx.db-journal` 文件
  2. 把修改直接写回主数据库文件
  3. 提交成功后删除 journal

问题出在第 2 步:写操作是**直接覆盖主库文件**的。所以在写入过程中,任何读操作都必须等它写完,否则可能读到写了一半的页。SQLite 用文件锁来实现这个互斥——写的时候持排他锁,读的时候持共享锁。读写不能并发,这就是卡顿的根源。

放到那个场景里就很好理解了:日志任务在批量写 10 万行,持写锁的时间被拉得很长,这期间所有接口的读请求全在排队。

二、WAL 改了什么

WAL(Write-Ahead Logging)的核心思路是把"原地覆盖"换成"追加写":

  • 所有修改先追加到 `xxx.db-wal` 文件,主库文件**暂时不动**
  • 读操作依然读主库,但会先在 WAL 里检查有没有该页的更新版本,有就用新的

这样一来,写只在追加 WAL,读在读主库,两者操作的是不同的文件区域,**天然不冲突**。多个读可以同时进行,读写也可以同时进行。

真正的"落地"发生在 checkpoint:当 WAL 文件增长到一定阈值(默认 1000 页),SQLite 会把 WAL 里的修改合并回主库。这一步需要短暂阻塞,但它是批量的,摊薄到单次事务上开销很小。

三、怎么开

只有一行 PRAGMA,而且只需要执行一次——WAL 是个**持久化设置**,写进数据库头,之后每次打开都自动生效:

PRAGMA journal_mode = WAL;

验证一下:

PRAGMA journal_mode;   -- 返回 wal 就成功了

如果你用 Python:

import sqlite3

conn = sqlite3.connect("app.db")
print(conn.execute("PRAGMA journal_mode=WAL").fetchone()) # ('wal',)
```

顺手还建议调两个参数:

PRAGMA synchronous = NORMAL;   -- WAL 下的推荐值,掉电最多丢最后一个事务
PRAGMA wal_autocheckpoint = 1000;  -- 默认值,一般不用改

`synchronous = NORMAL` 是官方在 WAL 模式下的推荐配置。在默认的 `FULL` 下,每次提交都要 fsync 两次;降到 `NORMAL` 后只在 checkpoint 时 fsync。代价是极端掉电场景可能丢失最近一个事务,但对大多数业务完全可接受——毕竟你本来也不该拿 SQLite 存"绝对不能丢"的核心账务数据。

四、代价和坑

WAL 不是银弹,有几个必须知道的限制:

**第一,产生了额外的文件。** 除 `app.db` 外,旁边会多出 `app.db-wal` 和 `app.db-shm`(共享内存索引)。备份的时候如果把 `app.db` 单独拷走,会漏掉 WAL 里还没 checkpoint 的数据。正确做法是用 `sqlite3 app.db ".backup backup.db"` 或者 `VACUUM INTO 'backup.db'`。

**第二,WAL 模式下不能跨网络文件系统。** NFS、SMB 这类共享挂载上,`shm` 文件的 mmap 行为不可靠,SQLite 会直接拒绝启用 WAL(退回 rollback journal),或者行为诡异。数据库文件必须放在本地盘上。

**第三,WAL 只能单写者。** 并发读解决了,但写入仍然是串行的——同一时刻只有一个连接能写。如果你的场景是"多进程同时大量写",SQLite 本身就不是合适的选择,该上 PostgreSQL 了。

**第四,只读副本的坑。** 如果数据库在只读介质上打开,WAL 相关的恢复流程可能失败。这种情况要显式指定 `PRAGMA journal_mode=DELETE` 或者用 `file:app.db?mode=ro&immutable=1`。

五、什么时候值得开

满足这几条就该开:

  • 读多写少,或读写混合但写不是高频
  • 数据库文件在本地盘
  • 有多个连接/进程同时访问
  • 读请求的延迟敏感(接口、实时查询)

如果只有一个进程、串行地读写、压根没有并发,那开了也没坏处,只是收益不明显。

反过来,如果写入非常密集、单机吞吐顶到 fsync 上限(一般几百到几千 TPS),WAL 也救不了你,因为瓶颈已经从锁变成了磁盘刷盘。这时候考虑批量事务、`BEGIN IMMEDIATE` 合并写,或者换数据库。

六、小结

WAL 的本质是把"原地修改"换成"追加日志 + 读时合并",用一份额外文件换掉了读写锁的互斥。它是一个只需要一行 PRAGMA、零迁移成本、收益却通常很显著的优化——如果你手上还有没开 WAL 的 SQLite,值得今天就打开看看。


*适用版本:SQLite 3.7.0+(2010 年引入 WAL)。文中参数以 3.40+ 实测为准。*

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。