预写记录

、概述

SQLite 实现原子提交和回滚的默认方法 回滚日志版本 3.7.0 (2010-07-21) 开始,可以使用新的“Write-Ahead Log”选项(以下简称“WAL”)。

使用 WAL 代替回滚日志有优点也有缺点。优势包括:

  1. 在大多数情况下,WAL 明显更快。
  2. WAL 提供了更多的并发性,因为读者不会阻止作者,而作者也不会阻止读者。读和写可以同时进行。
  3. 使用 WAL,磁盘 I/O 操作往往更加顺序。
  4. WAL 使用更少的 fsync() 操作,因此在 fsync() 系统调用被破坏的系统上不易受到问题的影响。

但也有缺点:

  1. WAL 通常要求VFS 支持共享内存原语。(例外:没有共享内存的 WAL)内置的 unix 和 windows VFSes 支持这个,但自定义操作系统的第三方扩展 VFSes 可能不支持。
  2. 使用数据库的所有进程必须在同一台主机上;WAL 不能在网络文件系统上工作。
  3. 涉及对多个ATTACHed 数据库进行更改的事务对于每个单独的数据库都是原子的,但对于作为一个集合的所有数据库而言并不是原子的。
  4. 进入 WAL 模式后无法更改page_size,无论是在空数据库上还是通过使用VACUUM或通过使用备份 API从备份恢复您必须处于回滚日志模式才能更改页面大小。
  5. 无法打开只读 WAL 数据库打开进程必须对与数据库关联的“ -shmwal-index共享内存文件具有写入权限(如果该文件存在),否则如果“ -shm ”文件不存在则对包含数据库文件的目录具有写入权限。版本 3.22.0 (2018-01-22) 开始,如果-shm-wal文件已经存在或者可以创建这些文件或者 数据库是不可变的,则可以打开只读 WAL 模式数据库文件
  6. 在主要读取很少写入的应用程序中,WAL 可能比传统的回滚日志方法稍微慢一点(可能慢 1% 或 2%)。
  7. 还有一个额外的准持久性“ -wal ”文件和“ -shm ”共享内存文件与每个数据库相关联,这会降低 SQLite 作为 应用程序文件格式的吸引力。
  8. 还有一个额外的检查点操作,虽然默认情况下是自动的,但仍然是应用程序开发人员需要注意的事情。
  9. WAL 最适用于较小的事务。WAL 不适用于非常大的事务。对于大于约 100 兆字节的事务,传统的回滚日志模式可能会更快。对于超过 1 GB 的事务,WAL 模式可能会因 I/O 或磁盘已满错误而失败。建议对大于几十兆字节的事务使用其中一种回滚日志模式。版本 3.11.0 (2016-02-15) 开始,WAL 模式与回滚模式一样有效地处理大型事务。

2. WAL 是如何工作的

传统回滚日志的工作方式是将原始未更改的数据库内容的副本写入单独的回滚日志文件,然后将更改直接写入数据库文件。在发生崩溃或ROLLBACK时,回滚日志中包含的原始内容将回放到数据库文件中,以将数据库文件恢复到其原始状态。COMMIT发生在回滚日志被删除时

WAL 方法颠倒了这一点。原始内容保留在数据库文件中,更改附加到单独的 WAL 文件中。指示提交的特殊记录附加到 WAL 时,将发生提交因此 COMMIT 可以在不写入原始数据库的情况下发生,这允许读者继续从原始未更改的数据库进行操作,同时将更改同时提交到 WAL 中。多个事务可以附加到单个 WAL 文件的末尾。

2.1. 检查点

当然,人们希望最终将附加在 WAL 文件中的所有事务传输回原始数据库。将 WAL 文件事务移回数据库称为“检查点”。

考虑回滚日志和预写日志之间区别的另一种方法是,在回滚日志方法中,有两种基本操作,即读和写,而对于预写日志,现在只有三种基本操作:读、写和检查点。

默认情况下,当 WAL 文件达到 1000 页的阈值大小时,SQLite 会自动执行检查点操作。SQLITE_DEFAULT_WAL_AUTOCHECKPOINT编译时选项可用于指定不同的默认值。)使用 WAL 的应用程序无需执行任何操作即可使这些检查点发生。但如果他们愿意,应用程序可以调整自动检查点阈值。或者他们可以关闭自动检查点并在空闲时间或在单独的线程或进程中运行检查点。

2.2. 并发

当读取操作在 WAL 模式数据库上开始时,它首先记住 WAL 中最后一个有效提交记录的位置。将此点称为“结束标记”。因为当各种读者连接到数据库时 WAL 可以增长并添加新的提交记录,所以每个读者都可能有自己的结束标记。但是对于任何特定的读取器,结束标记在事务期间是不变的,从而确保单个读取事务只能看到数据库内容,因为它存在于单个时间点。

当读者需要一页内容时,它首先检查 WAL 以查看该页面是否出现在那里,如果是,它会拉入 WAL 中出现在读者结束标记之前的页面的最后一个副本。如果在读取器结束标记之前 WAL 中不存在该页面的副本,则从原始数据库文件中读取该页面。读者可以存在于不同的进程中,因此为了避免强迫每个读者扫描整个 WAL 来寻找页面(WAL 文件可以增长到数兆字节,具体取决于运行检查点的频率),称为“wal-index”的数据结构维护在共享内存中,这有助于读者以最少的 I/O 快速定位 WAL 中的页面。wal-index 大大提高了读取器的性能,但是使用共享内存意味着所有读取器必须存在于同一台机器上。

编写器只是将新内容附加到 WAL 文件的末尾。因为写者不做任何会干扰读者行为的事情,所以写者和读者可以同时运行。但是,由于只有一个 WAL 文件,因此一次只能有一个写入器。

检查点操作从 WAL 文件中获取内容并将其传输回原始数据库文件。检查点可以与读取器同时运行,但是当检查点到达 WAL 中超过任何当前读取器结束标记的页面时,检查点必须停止。检查点必须在该点停止,否则它可能会覆盖读者正在使用的部分数据库文件。检查点会记住(在 wal-index 中)它到达了多远,并将在下一次调用时从它停止的地方恢复将内容从 WAL 传输到数据库。

因此,长时间运行的读取事务会阻止检查点取得进展。但大概每个读取事务最终都会结束,检查点将能够继续。

每当发生写入操作时,写入器检查检查点取得了多少进展,如果整个 WAL 已传输到数据库并同步,并且如果没有读者正在使用 WAL,则写入器将 WAL 倒回至开始并开始将新事务放在 WAL 的开头。此机制可防止 WAL 文件无限制地增长。

2.3. 性能注意事项

写入事务非常快,因为它们只涉及写入一次内容(相对于回滚日志事务的两次)并且因为写入都是顺序的。此外,不需要将内容同步到磁盘,只要应用程序愿意在断电或硬重启后牺牲持久性。(如果PRAGMA synchronous设置为 FULL, Writer 在每次事务提交时同步 WAL, 但如果PRAGMA synchronous设置为 NORMAL,则忽略此同步。)

另一方面,随着 WAL 文件大小的增长,读取性能会下降,因为每个读取器都必须检查 WAL 文件的内容,并且检查 WAL 文件所需的时间与 WAL 文件的大小成正比。wal-index 有助于更快地在 WAL 文件中查找内容,但性能仍然会随着 WAL 文件大小的增加而下降。因此,为了保持良好的读取性能,通过定期运行检查点来减小 WAL 文件的大小非常重要。

检查点确实需要同步操作,以避免断电或硬重启后数据库损坏的可能性。在将内容从 WAL 移动到数据库之前,必须将 WAL 同步到持久存储,并且必须在重置 WAL 之前同步数据库文件。检查站也需要更多的寻求。检查点会尽可能多地向数据库顺序写入页面(页面按升序从 WAL 传输到数据库),但即便如此,通常也会有许多寻道操作散布在页面写入之间。这些因素结合起来使检查点比写入事务慢。

默认策略是允许连续写入事务增加 WAL,直到 WAL 的大小变为大约 1000 页,然后为每个后续 COMMIT 运行检查点操作,直到 WAL 重置为小于 1000 页。默认情况下,检查点将由执行将 WAL 推到其大小限制的 COMMIT 的同一线程自动运行。这会导致大多数 COMMIT 操作非常快,但偶尔的 COMMIT(触发检查点的操作)会慢得多。如果这种效果不受欢迎,那么应用程序可以禁用自动检查点并在单独的线程或单独的进程中运行定期检查点。(完成此操作的命令和界面的链接 如下所示。)

请注意,将PRAGMA 同步设置为 NORMAL 时,检查点是唯一发出 I/O 屏障或同步操作的操作(unix 上的 fsync() 或 windows 上的 FlushFileBuffers())。因此,如果应用程序在单独的线程或进程中运行检查点,则执行数据库查询和更新的主线程或进程将永远不会阻塞同步操作。这有助于防止在繁忙的磁盘驱动器上运行的应用程序出现“闭锁”。此配置的缺点是事务不再持久并且可能会在电源故障或硬重置后回滚。

还要注意平均读取性能和平均写入性能之间存在权衡。为了最大化读取性能,人们希望使 WAL 尽可能小,因此经常运行检查点,可能与每次 COMMIT 一样频繁。为了最大限度地提高写入性能,人们希望将每个检查点的成本分摊到尽可能多的写入中,这意味着人们希望不频繁地运行检查点并让 WAL 在每个检查点之前增长得尽可能大。因此,运行检查点的频率决定可能因应用程序而异,具体取决于应用程序的相关读写性能要求。默认策略是在 WAL 达到 1000 页时运行检查点,并且该策略似乎在工作站上的测试应用程序中运行良好,

3.激活和配置WAL模式

SQLite 数据库连接默认为 journal_mode=DELETE要转换为 WAL 模式,请使用以下编译指示:

PRAGMA journal_mode=WAL;

journal_mode pragma 返回一个字符串,它是新的日志模式。成功时,pragma 将返回字符串“ wal ”。如果无法完成到 WAL 的转换(例如,如果VFS 不支持必要的共享内存原语),那么日志模式将保持不变,从原语返回的字符串将是先前的日志模式(例如“删除“)。

3.1. 自动检查点

默认情况下,每当发生导致 WAL 文件大小为 1000 页或更多的COMMIT 时,或者当数据库文件上的最后一个数据库连接关闭时,SQLite 将自动检查点。默认配置适用于大多数应用程序。但是需要更多控制的程序可以使用wal_checkpoint pragma或调用 sqlite3_wal_checkpoint() C 接口来强制检查点。使用wal_autocheckpoint pragma或调用 sqlite3_wal_autocheckpoint() C 接口可以更改自动检查点阈值或完全禁用自动检查点。一个程序也可以使用sqlite3_wal_hook()注册回调以在任何事务提交到 WAL 时调用。然后,此回调可以 根据它认为合适的任何标准调用sqlite3_wal_checkpoint()sqlite3_wal_checkpoint_v2() 。(自动检查点机制作为sqlite3_wal_hook()的简单包装器实现。)

3.2. 应用程序启动的检查点

应用程序只需调用sqlite3_wal_checkpoint()sqlite3_wal_checkpoint_v2()即可使用数据库上的任何可写数据库连接启动检查点 检查点分为三种子类型,其攻击性各不相同:被动、完整和重启。默认的检查点样式是 PASSIVE,它在不干扰其他数据库连接的情况下尽可能多地工作,并且如果有并发的读取器或写入器,它可能无法运行完成。由sqlite3_wal_checkpoint()和自动检查点机制启动的所有检查点都是被动的。FULL 和 RESTART 检查点会更加努力地运行检查点直至完成,并且只能通过调用sqlite3_wal_checkpoint_v2()来启动. 有关 FULL 和 RESET 检查点的更多信息,请参阅 sqlite3_wal_checkpoint_v2()文档。

3.3. WAL模式的持久化

与其他日志模式不同, PRAGMA journal_mode=WAL是持久的。如果进程设置 WAL 模式,然后关闭并重新打开数据库,数据库将返回 WAL 模式。相反,如果进程设置(例如)PRAGMA journal_mode=TRUNCATE 然后关闭并重新打开数据库将以默认回滚模式 DELETE 而不是以前的 TRUNCATE 设置恢复。

WAL 模式的持久化意味着应用程序可以转换为使用 WAL 模式的 SQLite,而无需对应用程序本身进行任何更改。只需使用命令行 shell或其他实用程序在数据库文件上运行“ PRAGMA journal_mode=WAL; ”,然后重新启动应用程序。

如果在任何一个连接上设置 WAL 日志模式,则将在所有连接到同一数据库文件上设置它。

4. WAL文件

数据库连接在 WAL 模式数据库上打开时,SQLite 会维护一个额外的日志文件,称为“预写日志”或“WAL 文件”。该文件在磁盘上的名称通常是带有额外“ -wal ”后缀的数据库文件的名称,但如果 SQLite 是使用SQLITE_ENABLE_8_3_NAMES编译的,则可能会应用不同的命名规则

只要任何数据库连接打开了数据库,WAL 文件就会存在。通常,当与数据库的最后一个连接关闭时,WAL 文件会自动删除。但是,如果打开数据库的最后一个进程在没有完全关闭数据库连接的情况下退出,或者如果 SQLITE_FCNTL_PERSIST_WAL 文件控制被使用,那么在关闭所有与数据库的连接后,WAL 文件可能会保留在磁盘上。WAL 文件是数据库持久状态的一部分,如果复制或移动数据库,则应与数据库一起保存。如果数据库文件与其 WAL 文件分离,那么之前提交到数据库的事务可能会丢失,或者数据库文件可能会损坏。删除 WAL 文件的唯一安全方法是使用sqlite3_open()接口之一打开数据库文件,然后立即使用sqlite3_close()关闭数据库。

WAL 文件格式被精确定义并且是跨平台的

5.只读数据库

旧版本的 SQLite 无法读取只读的 WAL 模式数据库。换句话说,需要写入权限才能读取 WAL 模式的数据库。从 SQLite版本 3.22.0 (2018-01-22) 开始放宽了此约束。

在较新版本的 SQLite 上,只读介质上的 WAL 模式数据库,或缺少写入权限的 WAL 模式数据库,只要满足以下一个或多个条件,仍然可以读取:

  1. -shm-wal文件已经存在并且 可读
  2. 对包含数据库的目录具有写入权限,以便可以创建 -shm-wal文件。
  3. 使用 不可变查询参数打开数据库连接。

尽管可以打开只读 WAL 模式数据库,但 在将 SQLite 数据库映像刻录到只读媒体之前转换为PRAGMA journal_mode=DELETE是一种很好的做法。

6.避免过大的 WAL 文件

在正常情况下,新内容会附加到 WAL 文件,直到 WAL 文件累积了大约 1000 页(因此大小约为 4MB),此时会自动运行检查点并回收 WAL 文件。检查点通常不会截断 WAL 文件(除非设置了journal_size_limit pragma)。相反,它只会导致 SQLite 从头开始​​覆盖 WAL 文件。这样做是因为覆盖现有文件通常比追加更快。当与数据库的最后一个连接关闭时,该连接会执行最后一个检查点,然后删除 WAL 及其关联的共享内存文件,以清理磁盘。

所以在绝大多数情况下,应用程序根本不需要担心 WAL 文件。SQLite 会自动处理它。但是有可能让 SQLite 进入 WAL 文件无限制增长的状态,导致磁盘空间使用过多和查询速度变慢。以下要点列举了可能发生这种情况的一些方式以及如何避免它们。

  • 禁用自动检查点机制。 在其默认配置中,当 WAL 文件长度超过 1000 页时,SQLite 将在任何事务结束时检查 WAL 文件。但是,存在可以禁用或推迟此自动检查点的编译时和运行时选项。如果应用程序禁用自动检查点,则没有什么可以防止 WAL 文件过度增长。

  • 检查站饥饿。 如果没有其他数据库连接使用 WAL 文件,检查点只能运行到完成并重置 WAL 文件。如果另一个连接打开了读取事务,则检查点无法重置 WAL 文件,因为这样做可能会删除读取器下的内容。检查点会在不打扰读者的情况下做尽可能多的工作,但它不能运行到完成。检查点将在下一次写事务后从它停止的地方再次启动。重复此过程,直到某个检查点能够完成。

    然而,如果一个数据库有许多并发的重叠读者,并且总是至少有一个活跃的读者,那么没有检查点能够完成,因此 WAL 文件将无限增长。

    可以通过确保存在“读取器间隔”来避免这种情况:没有进程正在从数据库读取并且在这些时间段内尝试检查点的时间。在具有许多并发读取器的应用程序中,还可以考虑使用SQLITE_CHECKPOINT_RESTARTSQLITE_CHECKPOINT_TRUNCATE选项运行手动检查点,这将确保检查点在返回前运行完成。使用 SQLITE_CHECKPOINT_RESTARTSQLITE_CHECKPOINT_TRUNCATE的缺点是读者可能会在检查点运行时阻塞。

  • 非常大的写入事务。 检查点只有在没有其他事务运行时才能完成,这意味着 WAL 文件不能在写入事务的中间重置。因此,对大型数据库的大型更改可能会导致大型 WAL 文件。一旦写入事务完成(假设没有其他读者阻止它),WAL 文件将被检查点,但与此同时,该文件可能会变得非常大。

    从 SQLite版本 3.11.0 (2016-02-15) 开始,单个事务的 WAL 文件的大小应与事务本身成比例。由事务更改的页面只应写入 WAL 文件一次。但是,对于旧版本的 SQLite,如果事务增长大于页面缓存,同一页面可能会多次写入 WAL 文件。

7. WAL-索引的共享内存的实现

wal-index是使用普通文件实现的,该文件为了稳健性而被映射WAL 模式的早期(预发布)实现将 wal-index 存储在易失性共享内存中,例如在 Linux 上的 /dev/shm 或其他 unix 系统上的 /tmp 中创建的文件。该方法的问题在于具有不同根目录的进程(通过chroot更改) 将看到不同的文件,因此使用不同的共享内存区域,从而导致数据库损坏。其他创建无名共享内存块的方法不能跨各种 unix 版本移植。而且我们找不到任何在 Windows 上创建无名共享内存块的方法。我们发现保证访问同一个数据库文件的所有进程使用同一个共享内存的唯一方法是通过在与数据库本身相同的目录中映射一个文件来创建共享内存。

使用一个普通的磁盘文件来提供共享内存的缺点是它实际上可能会通过将共享内存写入磁盘来进行不必要的磁盘 I/O。然而,开发人员认为这不是主要问题,因为 wal-index 的大小很少超过 32 KiB,而且从不同步。此外,wal-index 备份文件在最后一个数据库连接断开时被删除,这通常会阻止任何真正的磁盘 I/O 发生。

共享内存的默认实现不可接受的专用应用程序可以通过自定义VFS设计替代方法。例如,如果已知特定数据库将仅由单个进程中的线程访问,则可以使用堆内存而不是真正的共享内存来实现 wal-index。

8.在没有共享内存的情况下使用 WAL

从 SQLite版本 3.7.4 (2010-12-07) 开始,只要 在第一次尝试访问之前将locking_mode设置为 EXCLUSIVE,即使共享内存不可用,也可以创建、读取和写入 WAL 数据库。换句话说,如果保证该进程是访问数据库的唯一进程,则该进程可以在不使用共享内存的情况下与 WAL 数据库交互。此功能允许 WAL 数据库由缺少sqlite3_io_methods对象上的“版本 2”共享内存方法 xShmMap、xShmLock、xShmBarrier 和 xShmUnmap 的遗留VFSes创建、读取和写入。

如果在第一次 WAL 模式数据库访问之前设置了EXCLUSIVE 锁定模式 ,则 SQLite 永远不会尝试调用任何共享内存方法,因此永远不会创建共享内存 wal-index。在那种情况下,只要日志模式是 WAL,数据库连接就会保持在独占模式;尝试使用“ PRAGMA locking_mode=NORMAL; ”更改锁定模式是空操作。退出 EXCLUSIVE 锁定模式的唯一方法是首先退出 WAL 日志模式。

如果 NORMAL 锁定模式对第一次 WAL 模式数据库访问有效,则创建共享内存 wal-index。这意味着底层 VFS 必须支持“版本 2”共享内存。如果 VFS 不支持共享内存方法,那么尝试打开一个已经处于 WAL 模式的数据库,或者尝试将数据库转换为 WAL 模式,都会失败。只要只有一个连接使用共享内存 wal-index,锁定模式就可以在 NORMAL 和 EXCLUSIVE 之间自由切换。只有当共享内存 wal-index 被省略时,当锁定模式在第一次 WAL 模式数据库访问之前是 EXCLUSIVE 时,锁定模式才会停留在 EXCLUSIVE。

9.有时查询在 WAL 模式下返回 SQLITE_BUSY

WAL 模式的第二个优点是写者不会阻塞读者,读者也不会阻塞写者。大部分是正确的。但是有一些模糊的情况,其中对 WAL 模式数据库的查询可以返回SQLITE_BUSY,因此应用程序应该为这种情况做好准备。

对 WAL 模式数据库的查询可以返回SQLITE_BUSY 的情况包括:

  • 如果另一个数据库连接以独占锁定模式打开数据库模式,则所有针对该数据库的查询都将返回SQLITE_BUSYChrome 和 Firefox 都以独占锁定模式打开它们的数据库文件,因此在应用程序运行时尝试读取 Chrome 或 Firefox 数据库会遇到这个问题。

  • 当与特定数据库的最后一个连接关闭时,该连接将在清理 WAL 和共享内存文件时获得短时间的独占锁。如果第二个数据库尝试打开并查询数据库,而第一个连接仍在其清理过程中,则第二个连接可能会出现SQLITE_BUSY 错误。

  • 如果与数据库的最后一个连接崩溃,那么打开数据库的第一个新连接将启动恢复过程。恢复期间会持有独占锁。因此,如果第三个数据库连接试图在第二个连接正在运行恢复时跳入并查询,则第三个连接将收到SQLITE_BUSY错误。

10.向后兼容性

WAL 模式的数据库文件格式不变。但是,WAL 文件和wal-index是新概念,因此旧版本的 SQLite 将不知道如何恢复崩溃发生时以 WAL 模式运行的崩溃 SQLite 数据库。为了防止旧版本的 SQLite(3.7.0 之前的版本,2010-07-22)尝试恢复 WAL 模式数据库(并使事情变得更糟)数据库文件格式版本号(数据库头中的字节 18 和 19 ) 在 WAL 模式下从 1 增加到 2。因此,如果旧版本的 SQLite 尝试连接到以 WAL 模式运行的 SQLite 数据库,它将报告类似“文件已加密或不是数据库”的错误。

可以使用这样的编译指示显式地退出 WAL 模式:

PRAGMA journal_mode=DELETE;

故意退出 WAL 模式会将数据库文件格式版本号更改回 1,以便旧版本的 SQLite 可以再次访问数据库文件。