Menu

SQLite WAL 模式与并发:读者、写者和检查点

SQLite 的预写日志(write-ahead logging)如何改变并发方式:读者和写者不再互相阻塞,以及 -wal 和 -shm 文件到底是做什么的。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

默认模式及其局限

默认情况下,SQLite 使用回滚日志(rollback journal)。写入时,SQLite 先把原始页复制到 -journal 文件,再修改主数据库,提交时删除日志。如果进程在写入中途崩溃,就反向回放日志,撤销写了一半的改动。

这种方式简单又安全,但有一个让人头疼的特点:写者和读者争抢同一个文件。写者持有数据库锁时,任何读者都无法开始新事务;有读者在活动时,写者只能等待。在繁忙的应用里,比如一个同时处理几个请求的 Web 服务器,你会比预想中更早看到 SQLITE_BUSY 错误。

WAL 模式改变了这一点。

WAL 实际上做了什么

预写日志把模型反了过来。写者不再原地修改主数据库文件,而是把已提交的页追加到一个以 -wal 结尾的单独文件中。读者继续读取主文件,同时也会查看 WAL,找到所需页的较新版本。

结果是:一个写者和任意数量的读者可以在同一时刻活动。每个读者看到的是自己事务开始时的一致快照,写者则忙着往 WAL 里追加内容,不会碰到读者正在看的数据。

这一条 pragma 就能切换数据库的模式。这个模式是持久的:它保存在文件头里,之后的每个连接都会自动使用 WAL。你不需要在每个连接上执行它,只要在创建数据库时(或在迁移脚本里)执行一次即可。

这条 pragma 会返回新的模式。如果返回 wal,就说明设置成功了。如果返回别的值,很可能是文件系统不支持共享内存(下文会详细说明)。

开启与验证

你随时可以确认当前的模式:

第一条调用开启 WAL 并返回新模式,第二条(不带 =)只是查询当前模式。之后,只要有活动,messages.db 所在目录里就会有三个文件:messages.db、messages.db-wal 和 messages.db-shm。后两个文件会随着连接的打开和关闭出现或消失。

-wal 和 -shm 文件

WAL 会带来两个额外的文件,你应该了解它们的作用:

  • -wal 保存已经提交、但还没合并回主数据库的事务。它随着写入不断变大,在检查点时缩小(或重置)。
  • -shm 是一个共享内存文件。它是 WAL 的索引,让所有连接对“哪个页在哪里”达成一致,而不必每次查询都扫描 WAL。

实际影响是:复制 WAL 模式的数据库时,绝不能只复制 .db 文件。最新的数据在 -wal 里,少了它,你的副本要么是过时的,要么已经损坏。要么在没有连接写入时把三个文件一起复制,要么(更好的做法)使用 SQLite 备份 API(下一章会讲到)。

并发:一个写者,多个读者

WAL 并不能让你并发写入。SQLite 仍然让写入逐个进行:任何时刻都只有一个事务持有写锁。改变的是,写不阻塞读,读也不阻塞写。

所以,一个运行在 WAL 上的典型 Web 应用表现如下:

  • 以读为主的接口并行运行,互不争抢。
  • 写接口会短暂排队,但不会阻塞读取。
  • 长时间运行的读者(分析查询、数据导出)不会让写者等待。

如果两个连接同时尝试写入,第二个会得到 SQLITE_BUSY。通常的解决办法是设置一个合理的忙等待超时,让 SQLite 先等一会儿再放弃:

busy_timeout=5000 的意思是“如果锁被占用,最多等 5 秒再报错”。配合 WAL,这足以应对大多数应用实际遇到的争用。BEGIN IMMEDIATE 会在事务开始时就获取写锁,而不是等到第一次写入时,这样当多个连接都打算写入时,可以避免一类锁升级导致的死锁。

检查点:把 WAL 合并回去

WAL 文件不能无限增长。检查点(checkpoint)就是把 WAL 中已提交的页写入主数据库,然后重置 WAL 的过程。

当 WAL 超过约 1000 页(默认的 wal_autocheckpoint)时,SQLite 会自动执行检查点。大多数应用保持默认即可。如果你想调整它,或者手动触发一次:

wal_checkpoint pragma 接受一个模式:

  • PASSIVE:在不打扰读者和写者的前提下尽量完成检查点。这是默认模式。
  • FULL:等待活动中的写者完成,然后把所有已提交的内容写入检查点。
  • RESTART:与 FULL 相同,另外阻止新的读者使用旧的 WAL。
  • TRUNCATE:与 RESTART 相同,另外把 WAL 文件截断为零字节。

大多数服务器从不需要手动调用它。如果你在开发一个桌面应用,希望关闭时让文件大小保持整洁,那么在关闭最后一个连接之前执行一次 TRUNCATE 检查点是个不错的习惯。

与 WAL 搭配使用的几个 pragma

单独使用 WAL 已经很好。生产环境的应用通常会把 WAL 和另外几个设置一起使用:

简单介绍一下:

  • synchronous=NORMAL 是推荐与 WAL 搭配的设置。它能抵御应用崩溃和操作系统崩溃;只有恰好在错误的时刻断电,才可能丢失最近的几个事务,即便如此数据库也会保持一致。默认的 FULL 更安全,但明显更慢。
  • busy_timeout 上文已经讲过。
  • foreign_keys=ON 与 WAL 无关,但值得在每个连接上设置:为了向后兼容,SQLite 默认不检查外键约束。

这些设置都只对当前连接生效(journal_mode 除外,它会保留下来)。在应用代码中打开连接后立刻执行它们。

什么时候不该用 WAL

WAL 是默认推荐的选择,但有几种情况并不适合:

  • 网络文件系统。 WAL 依赖访问数据库的各个进程之间的共享内存(mmap)。NFS、SMB 之类的文件系统无法可靠地支持这一点。如果你的数据库放在网络共享上,就继续使用回滚日志;更好的做法是,不要把 SQLite 放在网络共享上。
  • 只读介质。 WAL 需要写入 -wal 和 -shm 文件。放在 CD-ROM 之类介质上的数据库必须使用不写文件的日志模式(或者用 mode=ro 以只读方式打开)。
  • 没有并发读者的单写者批处理任务。 WAL 不会带来坏处,但你也得不到任何好处。默认的回滚日志就够用了。

对于 95% 的应用,包括 Web 后端、桌面应用、移动应用、带本地存储的嵌入式设备,WAL 都是正确的选择。

一套贴近实际的配置

下面是大多数生产环境 SQLite 配置的样子,浓缩成了可以运行的 pragma:

temp_store=MEMORY 把临时表和索引放在内存而不是磁盘上。如果内存有富余,这是一个零成本的小优化。

在应用的数据库初始化代码里,于建立连接时把这些设置好,你就解决了 SQLite 应用在并发负载下保持良好表现所需的大部分工作。

下一章:备份与恢复

既然你的数据库现在多了 -wal 和 -shm 两个伙伴文件,直接复制文件就不再是安全的备份方式了。下一章介绍备份运行中的 SQLite 数据库的正确方法:.backup 命令、在线备份 API,以及在不让应用下线的情况下获取一致快照的办法。

常见问题

SQLite 中的 WAL 模式是什么?

WAL 是 write-ahead logging(预写日志)的缩写。SQLite 不再把改动直接写进主数据库文件、出错时再靠回滚日志撤销,而是把改动追加到单独的 -wal 文件里,定期再合并回主文件。最大的好处在并发上:读者和一个写者可以同时工作,互不阻塞。

SQLite 中如何开启 WAL 模式?

执行一次 PRAGMA journal_mode=WAL; 即可。这个设置是持久的:它保存在数据库文件头里,所以之后的连接都会自动使用 WAL。你不需要在每个连接上设置它。执行成功时,这条 pragma 会返回新的模式(wal)。

WAL 模式支持并发写入吗?

不支持。SQLite 仍然让写入逐个进行,同一时间只有一个写者能持有写锁。WAL 改变的是:读者不再阻塞写者,写者也不再阻塞读者。对大多数应用来说,真正的瓶颈正是这一点。

-wal 和 -shm 文件是什么?

-wal 文件保存已经提交、但还没合并回主数据库的改动。-shm 文件是一个很小的共享内存索引,帮助各个连接快速找到 WAL 里的页。两者都会自动重新创建,但如果你要复制数据库,就必须把它们一起复制,或者使用备份 API。

Coddy 编程语言插图

和 Coddy 一起学编程

开始