小巧。快速。可靠。
三选二。
预写日志

1. 概述

SQLite 实现原子提交和回滚的默认方法是回滚日志。从3.7.0 版本(2010-07-21)开始,提供了一个新的“预写日志”选项(以下简称“WAL”)。

与使用回滚日志相比,使用 WAL 有优点也有缺点。优点包括:

  1. 在大多数情况下,WAL 明显更快。
  2. WAL 提供了更高的并发性,因为读取器不会阻塞写入器,而写入器也不会阻塞读取器。读取和写入可以并发进行。
  3. 使用 WAL,磁盘 I/O 操作往往更具顺序性。
  4. WAL 使用的 fsync() 操作要少得多,因此在 fsync() 系统调用出现故障的系统上,其对问题的抵抗力更强。

但也存在一些缺点:

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

2. WAL 的工作原理

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

WAL 方法颠倒了这一点。原始内容保留在数据库文件中,更改被追加到一个单独的 WAL 文件中。COMMIT发生在指示提交的特殊记录被追加到 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 索引”的数据结构,它可以帮助读取器快速并在最小 I/O 的情况下在 WAL 中定位页面。WAL 索引大大提高了读取器的性能,但共享内存的使用意味着所有读取器都必须存在于同一台机器上。这就是为什么预写日志实现无法在网络文件系统上工作的原因。

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

检查点操作将内容从 WAL 文件传输回原始数据库文件。检查点可以与读取器并发运行,但是当检查点到达 WAL 中超过任何当前读取器结束标记的页面时,它必须停止。检查点必须在这一点停止,否则它可能会覆盖读取器正在积极使用的数据库文件的一部分。检查点会记住(在 WAL 索引中)它到达的位置,并在下次调用时从停止的地方继续将内容从 WAL 传输到数据库。

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

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

2.3. 性能考虑

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

另一方面,随着 WAL 文件大小的增加,读取性能会下降,因为每个读取器都必须检查 WAL 文件以获取内容,并且检查 WAL 文件所需的时间与 WAL 文件的大小成正比。WAL 索引可以帮助更快地找到 WAL 文件中的内容,但性能仍然会随着 WAL 文件大小的增加而下降。因此,为了保持良好的读取性能,务必通过定期运行检查点来减小 WAL 文件的大小。

检查点确实需要同步操作,以避免在断电或硬重启后发生数据库损坏的可能性。在将内容从 WAL 移入数据库之前,必须将 WAL 同步到持久存储,并且在重置 WAL 之前,必须同步数据库文件。检查点还需要更多的寻道操作。检查点会尽力对数据库进行尽可能多的顺序页面写入(页面按升序从 WAL 传输到数据库),但即使这样,通常也会在页面写入之间穿插许多寻道操作。这些因素共同导致检查点比写入事务慢。

默认策略是允许连续的写事务使WAL增长,直到WAL的大小达到大约1000页,然后对每个后续的COMMIT运行一个检查点操作,直到WAL的大小重置为小于1000页。默认情况下,检查点将由执行COMMIT的同一线程自动运行,该COMMIT使WAL超过其大小限制。这导致大多数COMMIT操作非常快,但偶尔的COMMIT(触发检查点的COMMIT)会慢得多。如果这种效果不希望出现,则应用程序可以禁用自动检查点并在单独的线程或单独的进程中运行周期性检查点。(实现此目的的命令和接口链接如下所示。)

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

还要注意,平均读取性能和平均写入性能之间存在权衡。为了最大化读取性能,需要使WAL尽可能小,因此需要频繁运行检查点,也许每个COMMIT运行一次。为了最大化写入性能,需要在尽可能多的写入上摊销每个检查点的成本,这意味着需要不频繁地运行检查点,并在每次检查点之前让WAL增长到尽可能大。因此,运行检查点的频率的决定可能因应用程序而异,具体取决于应用程序的相对读写性能要求。默认策略是在WAL达到1000页时运行一次检查点,并且此策略似乎在工作站上的测试应用程序中运行良好,但其他策略可能在不同的平台或不同的工作负载下效果更好。

3. 激活和配置WAL模式

SQLite数据库连接默认使用journal_mode=DELETE。要转换为WAL模式,请使用以下pragma

PRAGMA journal_mode=WAL;

journal_mode pragma返回一个字符串,该字符串是新的日志模式。成功后,pragma将返回字符串"wal"。如果无法完成转换为WAL(例如,如果VFS不支持必要的共享内存原语),则日志模式将保持不变,并且从原语返回的字符串将是之前的日志模式(例如"delete").

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、FULL和RESTART。默认的检查点样式为PASSIVE,它在不干扰其他数据库连接的情况下尽可能多地工作,如果存在并发读取器或写入器,则可能无法完成运行。由sqlite3_wal_checkpoint()和自动检查点机制启动的所有检查点都是PASSIVE。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,而无需对应用程序本身进行任何更改。只需在数据库文件上运行"PRAGMA journal_mode=WAL;" 使用命令行shell或其他实用程序,然后重新启动应用程序。

如果在一个连接上设置了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文件将无限增长,导致磁盘空间使用过量和查询速度变慢。以下要点列举了可能发生这种情况的一些方式以及如何避免它们。

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

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

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

对于默认共享内存实现不可接受的特殊应用程序可以通过自定义VFS设计替代方法。例如,如果已知特定数据库仅会被单个进程内的线程访问,则可以使用堆内存而不是真正的共享内存来实现WAL索引。

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

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

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

如果在第一次WAL模式数据库访问时生效的是NORMAL锁定模式,则会创建共享内存WAL索引。这意味着底层VFS必须支持“版本2”共享内存。如果VFS不支持共享内存方法,则尝试打开已处于WAL模式的数据库或尝试将数据库转换为WAL模式将失败。只要只有一个连接使用共享内存WAL索引,就可以在NORMAL和EXCLUSIVE之间自由更改锁定模式。只有在省略共享内存WAL索引时,即在第一次WAL模式数据库访问之前锁定模式为EXCLUSIVE时,锁定模式才会卡在EXCLUSIVE状态。

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

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

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

10. 向后兼容性

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

可以使用类似这样的pragma显式退出WAL模式

PRAGMA journal_mode=DELETE;

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

此页面上次修改于2024-07-14 23:07:20 UTC