很多人提到 SQLite 的 WAL 模式,习惯把它说成“把并发问题解决了”的那个开关。更贴近工程现实的理解,是它重新安排了阻塞关系,却没有放宽 SQLite 的协调边界。写事务仍然一次只容纳一个,checkpoint 从此进入日常性能路径,所有参与进来的进程也仍然要落在同一台主机上。[1][2]

团队评估 SQLite 是否还能留在当前架构里,真正有分量的问题,落在工作负载能否把写事务压短,把读事务收住,让 checkpoint 持续往前走。benchmark 能打出多少 reads/sec,分量反而靠后一些。这才是它的合同正文。

1)WAL 改写的是写入路径,拓扑边界仍然很窄

在 rollback journal 模式下,SQLite 直接改写主数据库文件,并把旧页面放进回滚日志里。切到 WAL 模式后,变更先追加到独立的 WAL 文件,再由后续 checkpoint 回写到主数据库。[1]

并发收益正是从这里长出来的。读事务继续读取主数据库文件,写事务把新 frame 追加进 WAL,旧有 rollback journal 路径上的正面冲突于是松开了。[1] 这层收益很实在,系统形状仍然紧凑:

第二条常常在架构讨论里被说轻了。WAL 扩展的是 SQLite 作为本地文件数据库的活动空间,跨主机共享协调仍然留在 SQLite 的边界之外。

2)reader end mark 把 checkpoint 变成真正的压力中心

SQLite 的 WAL 文档把核心机制解释得很清楚。每个读事务开始时,都会记住自己看到的 WAL 末端位置,也就是它的 snapshot 边界。这个位置之后再追加进来的 frame,已经落在这个读者的视野之外。[1]

问题从 checkpoint 开始显影。checkpoint 负责把 WAL 内容搬回主数据库文件,但它推进到某个位置时,如果那里之后的 frame 仍然被某个更早启动的读事务需要,它就要停下来。[1] 文档同时写明,默认的自动 checkpoint 阈值是 1000 pages。[1]

于是,长时间存在的读事务很容易变成第一类真问题:

顺着这个角度去看,WAL 把竞争从传统的读写锁边缘,转移到了 checkpoint debt(checkpoint 债务) 这条线上。Ten Thousand Meters 的独立工程分析把这件事说得更接地气:长读事务会把系统拖进 checkpoint starvation,写入量保持在常见区间,体感上的“越来越慢”已经会出现。[5]

3)写事务持续多久,就是并发预算本身

事务文档给出的硬边界很直接:SQLite 允许多个读事务同时存在,写事务同时只能有一个。[2] 在 WAL 模式下,BEGIN IMMEDIATE 会立刻启动写事务;BEGIN DEFERRED 要等第一条写语句出现时才升级;BEGIN EXCLUSIVE 在 WAL 下与 IMMEDIATE 的行为一致。[2]

由此展开,真正决定并发质量的,往往落在每个写者把锁拿在手里多久。

应用代码一旦在事务开启后还要等待 HTTP 调用、重试业务逻辑、跑一段很慢的上游流程,再去 COMMIT,后面的写者就会整齐排队。Ten Thousand Meters 那篇文章的价值,就在于它把 SQLite 的形式规则翻译成了团队更熟悉的故障表情:database is locked 这种报错,背后常常站着一个拖得过长的写路径。[5]

放进设计评审里,最值得问的一句是:事务开始以后,到提交之前,系统还夹带了多少与 SQL 无关的工作。这个指标,往往比平均查询延迟更能说明问题。

4)PRAGMA 调的是压力形状,不会把压力拿走

SQLite 给了几组直接控制 WAL 行为的开关。PRAGMA journal_mode=WAL 用来启用 WAL;PRAGMA wal_autocheckpoint 设置自动 checkpoint 的页面阈值;PRAGMA busy_timeout 则规定连接在数据库被锁住时愿意等待多久,再返回 SQLITE_BUSY。[3]

这些参数有用,是因为它们会改变故障的展开方式:

用得稳,这些 PRAGMA 像压力调节阀;用得草率,它们只是在把原本该收短的事务路径往后拖。

5)部署边界依然很好说清

SQLite 在 “Appropriate Uses” 页面里把适配范围写得很明确。它面向本地数据、嵌入式系统、应用文件、缓存,以及其他由单个应用或单台设备持有的持久状态。客户端/服务器型数据库更适合多台机器并发访问同一份数据,或把集中式管理与更高写入并发放在需求中心的位置。[4]

换成架构评审的语言,WAL 较稳的落点通常是:

风险上升的区域也很清楚:

从 SQLite 官方给出的边界继续往前推一步,WAL 最强的时候,往往是一支团队完整拥有写路径,能把每个事务从调用点一路看到提交点。等数据库开始承担共享协调面的角色,WAL 的优势就会很快收窄。

结语

SQLite WAL 更适合被看成一笔精确的交换。它通过 append-first 的写入路径和显式 checkpoint,换来了读写重叠的空间;与之相伴的,是一套始终收束得很紧的系统边界:一次一个写者、同主机协调、性能高度依赖事务长度与 checkpoint 纪律。[1][2][3]

工作负载若正好落在这组边界里面,WAL 是把持久 SQL 状态留在应用边界内的极稳方案。若系统不断朝多主机写入协调、长读事务并存、锁窗口难以收短的方向延伸,架构已经在提示另一种数据库形状。[4][5]

来源

  1. SQLite,《Write-Ahead Logging》—— WAL 设计、reader end mark、自动 checkpoint 与同主机边界。
  2. SQLite,《Transaction》—— 单写者规则、读写事务行为,以及 DEFERRED / IMMEDIATE / EXCLUSIVE 语义。
  3. SQLite,《PRAGMA Statements》—— journal_modewal_autocheckpointbusy_timeout 控制项。
  4. SQLite,《Appropriate Uses For SQLite》—— 本地状态适配边界,以及与客户端/服务器数据库的分界。
  5. Ten Thousand Meters,《SQLite, concurrent writes, and database is locked errors》—— 对 WAL 模式争用与 checkpoint starvation 的独立工程分析。