1. Overview
SQLite 实现 atomic commit and rollback 的方式是 rollback journal。从 version 3.7.0 开始,一个新的 “Write-Ahead Log” 选项可用。使用 WAL 而非 rollback joural 有优点也有缺点。优点包含:
- 在大部分场景下,WAL 明显更快;
- WAL 提供更多并发,因为 readers 和 writers 相互间不阻塞。读和写能同时进行;
- 磁盘 I/O 倾向于更连续;
- WAL 使用更少的 fsync() 操作,这样就更少的受到系统问题的影响,当 fsync() 系统调用失效。
但是也有一些缺点:
- WAL 通常需要 支持 shared-memory 基元的 VFS。
- 所有使用数据库的进程必须在相同的计算机上;WAL 不能跨网络文件系统工作。
- 涉及对多个 ATTACHed 数据库更改的事务对每个数据库都是 atomic 的,但是对数据库集合来说不是 atomic 的。
- 进入 WAL 模式后,不可能修改 page_size,无论是在空数据库上,还是通过使用 VACUUM 或通过使用 backup API 从备份中恢复。你必须在 journal 模式下改变 page size。
- 从 version 3.22.0 开始,read-only WAL-mode 数据库文件可以被打开,如果 -shm 和 -wal 文件已经存在或这些文件能够被创建,或者 database is immutable。
- WAL 可能相较传统的 rollback-journal 稍微慢点(或许慢个 %1 到 2%),如果应用经常读不常写。
- 有一个额外的准持久化的 “-wal” 文件或 “-shm” 共享内存文件关联每个数据库,这使 SQLite 更不会被用作应用文件格式。
- 有关于 checkpoint 的额外操作,虽然原子性是默认行为,但是仍然有一些东西应用开发者依然需要关心。
2. How WAL Works
传统的 rollback journal 写以原始的未改变的数据库内容到一个单独的 rollback journal 文件中作为副本,然后直接将改变写到数据库文件中。在 crash 或 ROLLBACK 事件中,rollback journal 中包含的原始内容被写回到数据库文件中以恢复数据库文件到它最初的状态。当 rollback journal 被删除时,COMMIT 发生。
WAL 方法使上述行为互换。原始的内容保存在数据库文件中,改变被追加到单独的 WAL 文件中。当一条特殊的记录表明 commit 被追加到 WAL 中, COMMIT 发生。因此 COMMIT 甚至能在没有将改变写入到数据库文件中时发生,这允许 reader 能在最初的未被改变的数据库上继续运行,与此同时改变能够被同时提交到 WAL 中。多个事务能够被追加到一个 WAL 文件的文件尾。
2.1 Checkpointing
当然,最终希望将所有追加到 WAL 文件的事务转移到原始的数据库中。移动 WAL 文件事务返回到数据库被称为一个 “checkpoint”。
考虑 rollback 和 write-ahead log 的不同之处的另一种方式是,在 rollback-journal 方式中,有两个基元操作,reading 和 writing,而在 write-ahead log 有三个基元操作:reading,writing 和 checkpointing。
默认情况下,当 WAL 文件到达 1000 pages 的阈值时,SQLite 自动的做 checkpoint 工作。使用 WAL 的应用不需要为这些 checkpoint 的发生做任何工作。但是如果需要的话,应用能够自动调整 checkpoint 阈值,或者可以关闭自动 checkpoint 的功能, 并在空闲时间做 checkpoint 或者在单独的线程或进程中做。
2.2 Concurreny
当读操作在 WAL-mode 的数据库上开始,它首先记住 WAL 中上一次合法的 commit 记录的位置,把这个点称为 “end mark”。因为当不同的 readers 连接数据库时,WAL 能够增长并添加新的 commit records,每个 reader 潜在的拥有它自己的 end mark。但是对任何特定的 reader,在 transaction 期间,end mark 保持不变。这保证了单独的 read transaction 仅可见它存在的那个单独时间点时的数据库内容。
当 reader 需要 a page of content,它先检查 WAL 文件中是否有该 page,如果有他将先于 reader 的 end mark 的最后一个 page 的拷贝 pull in。如果在 WAL 文件中不存在先于 reader 的 end mark 的 page 的拷贝,则 page 从原始的数据库文件中读取。readers 能存在于不同的进程中,所以为了避免强制每个 reader 扫描整个 WAL 文件来寻找 pages,(WAL 文件能增长到多兆字节,这取决于checkpoint 运行的多么频繁),一个被称为 “wal-index” 的数据结构被维护于共享内存中,来帮助 readers 快速定位在 WAL 文件中的 pages,并使 I/O 最小化。wal-index 极大的提高了 readers 的性能,但是使用共享内存意味着所有的 readers 必须存在于同一机器上。这就是为什么 write-ahead log 实现不能再网络文件系统中工作。
Writers 仅追加新的内容到 WAL 文件的尾端。因为 writers 没有做任何干扰 readers 行为的事,reader 和 writer 能够同时运行。然而,因为只有一个 WAL 文件,所以同时仅能有一个 writer 工作。
Writers 仅能追加新的内容到 WAL 文件的尾部并将其转移到原始的数据库文件。
一次 checkpoint 操作取出 WAL 文件的内容并转移到原始的数据库文件中。一次 checkpoint 操作能够与 readers 同时运行,然而 checkpoint 必须在到达当前任一 reader 的 end mark 之后的 page 时停止,否则它可能会改写 reader 正在使用的数据库文件的部分。checkpoint 会记住(在 wal-index 中)它已经走了多远,且在下次调用时将从上次停止的地方恢复转移 WAL 中的内容到 database 中。
因此一个 long-running 的 read transaction 可能阻止 checkpoint 取得进展。但是可能每个 read transaction 将最终结束且 checkpoint 将能够继续进行。
无论何时一个 write 行为发生,writer 检查 checkpointer 取得了多少进展,且如果整个 WAL 被转移到了数据库中且同步了,且没有 readers 正在使用 WAL,随后 writer 将会将 WAL 倒回到起始点,并开始将新的 transaction 放到 WAL 的起始点。这一机制保证 WAL 文件不会有超出界限的增长。