SQLite 实现原子提交和回滚的默认方法是回滚日志。从3.7.0 版本(2010-07-21)开始,提供了一个新的“预写日志”选项(以下简称“WAL”)。
与使用回滚日志相比,使用 WAL 有优点也有缺点。优点包括:
但也存在一些缺点:
传统的回滚日志的工作原理是将原始未更改数据库内容的副本写入一个单独的回滚日志文件,然后将更改直接写入数据库文件。如果发生崩溃或ROLLBACK,则将回滚日志中包含的原始内容回放回数据库文件,以将数据库文件恢复到其原始状态。COMMIT发生在回滚日志被删除时。
WAL 方法颠倒了这一点。原始内容保留在数据库文件中,更改被追加到一个单独的 WAL 文件中。COMMIT发生在指示提交的特殊记录被追加到 WAL 时。因此,COMMIT 可以发生在从未写入原始数据库的情况下,这允许读取器继续从原始未更改的数据库中操作,而更改则同时提交到 WAL 中。多个事务可以追加到单个 WAL 文件的末尾。
当然,人们最终希望将所有追加到 WAL 文件中的事务都转移回原始数据库。将 WAL 文件事务移回数据库称为“检查点”。
另一种看待回滚和预写日志之间区别的方法是,在回滚日志方法中,有两个基本操作,读取和写入,而在预写日志中,现在有三个基本操作:读取、写入和检查点。
默认情况下,当 WAL 文件大小达到 1000 页的阈值时,SQLite 会自动执行检查点。(SQLITE_DEFAULT_WAL_AUTOCHECKPOINT 编译时选项可用于指定不同的默认值。)使用 WAL 的应用程序无需执行任何操作即可执行这些检查点。但如果需要,应用程序可以调整自动检查点阈值。或者,他们可以关闭自动检查点,并在空闲时或在单独的线程或进程中运行检查点。
当在 WAL 模式数据库上开始读取操作时,它首先会记住 WAL 中最后一个有效提交记录的位置。将此点称为“结束标记”。因为 WAL 可能会在各种读取器连接到数据库时增长并添加新的提交记录,所以每个读取器都可能拥有自己的结束标记。但对于任何特定的读取器,结束标记在其事务期间保持不变,从而确保单个读取事务仅查看数据库内容在其某个时间点的状态。
当读取器需要一个内容页面时,它首先检查 WAL 以查看该页面是否出现在其中,如果出现,则提取 WAL 中在读取器结束标记之前出现的该页面的最后一个副本。如果在读取器结束标记之前 WAL 中不存在该页面的副本,则从原始数据库文件中读取该页面。读取器可以存在于不同的进程中,因此为了避免强制每个读取器扫描整个 WAL 以查找页面(WAL 文件可能会增长到几兆字节,具体取决于检查点的运行频率),在共享内存中维护了一个称为“WAL 索引”的数据结构,它可以帮助读取器快速并在最小 I/O 的情况下在 WAL 中定位页面。WAL 索引大大提高了读取器的性能,但共享内存的使用意味着所有读取器都必须存在于同一台机器上。这就是为什么预写日志实现无法在网络文件系统上工作的原因。
写入器只是将新内容追加到 WAL 文件的末尾。因为写入器不会执行任何可能干扰读取器操作的操作,所以写入器和读取器可以同时运行。但是,由于只有一个 WAL 文件,因此一次只能有一个写入器。
检查点操作将内容从 WAL 文件传输回原始数据库文件。检查点可以与读取器并发运行,但是当检查点到达 WAL 中超过任何当前读取器结束标记的页面时,它必须停止。检查点必须在这一点停止,否则它可能会覆盖读取器正在积极使用的数据库文件的一部分。检查点会记住(在 WAL 索引中)它到达的位置,并在下次调用时从停止的地方继续将内容从 WAL 传输到数据库。
因此,长时间运行的读取事务可以阻止检查点取得进展。但可以假设每个读取事务最终都会结束,检查点将能够继续。
每当发生写入操作时,写入器都会检查检查点取得了多少进展,如果整个 WAL 都已传输到数据库并已同步,并且没有读取器正在使用 WAL,则写入器会将 WAL 倒回开头,并开始将新事务放在 WAL 的开头。此机制可防止 WAL 文件无限增长。
写入事务非常快,因为它们只需要写入内容一次(而回滚日志事务需要写入两次),并且因为写入都是顺序的。此外,只要应用程序愿意在断电或硬重启后牺牲持久性,就不需要将内容同步到磁盘。(如果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页时运行一次检查点,并且此策略似乎在工作站上的测试应用程序中运行良好,但其他策略可能在不同的平台或不同的工作负载下效果更好。
SQLite数据库连接默认使用journal_mode=DELETE。要转换为WAL模式,请使用以下pragma
PRAGMA journal_mode=WAL;
journal_mode pragma返回一个字符串,该字符串是新的日志模式。成功后,pragma将返回字符串"wal"。如果无法完成转换为WAL(例如,如果VFS不支持必要的共享内存原语),则日志模式将保持不变,并且从原语返回的字符串将是之前的日志模式(例如"delete").
默认情况下,只要发生导致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()的简单包装器实现的。)
应用程序可以通过简单地调用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()文档。
与其他日志模式不同,PRAGMA journal_mode=WAL是持久性的。如果一个进程设置了WAL模式,然后关闭并重新打开数据库,则数据库将以WAL模式恢复。相反,如果一个进程设置(例如)PRAGMA journal_mode=TRUNCATE,然后关闭并重新打开数据库,则数据库将以默认的回滚模式DELETE恢复,而不是之前的TRUNCATE设置。
WAL模式的持久性意味着应用程序可以转换为使用WAL模式下的SQLite,而无需对应用程序本身进行任何更改。只需在数据库文件上运行"PRAGMA journal_mode=WAL;" 使用命令行shell或其他实用程序,然后重新启动应用程序。
如果在一个连接上设置了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文件格式已精确定义并且是跨平台的。
旧版本的SQLite无法读取处于只读状态的WAL模式数据库。换句话说,需要写入权限才能读取WAL模式数据库。从SQLite 3.22.0版(2018-01-22)开始,此约束已得到放宽。
在较新版本的SQLite上,只要满足以下一个或多个条件,就可以读取只读媒体上的WAL模式数据库,或者缺少写入权限的WAL模式数据库
即使可以打开只读WAL模式数据库,在将SQLite数据库映像刻录到只读媒体之前,最好将其转换为PRAGMA journal_mode=DELETE。
在正常情况下,新的内容会追加到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_RESTART或SQLITE_CHECKPOINT_TRUNCATE选项运行手动检查点,这将确保检查点在返回之前完成运行。使用SQLITE_CHECKPOINT_RESTART和SQLITE_CHECKPOINT_TRUNCATE的缺点是,在检查点运行期间,读取器可能会被阻塞。
非常大的写事务。只有在没有其他事务运行时,检查点才能完成,这意味着在写事务的中间无法重置WAL文件。因此,对大型数据库的大量更改可能会导致大型WAL文件。在写事务完成后(假设没有其他读取器阻止它),WAL文件将被检查点,但在那之前,文件可能会变得非常大。
从SQLite 3.11.0版(2016-02-15)开始,单个事务的WAL文件的大小应与其自身成正比。事务更改的页面应仅写入WAL文件一次。但是,在旧版本的SQLite中,如果事务大小超过页面缓存,则同一页面可能会多次写入WAL文件。
WAL索引使用一个普通的、为了鲁棒性而内存映射的文件来实现。早期(预发布)版本的WAL模式将WAL索引存储在易失性共享内存中,例如在Linux上创建的/dev/shm中的文件或其他Unix系统上的/tmp中的文件。这种方法的问题是,具有不同根目录(通过chroot更改)的进程将看到不同的文件,因此使用不同的共享内存区域,从而导致数据库损坏。其他创建无名共享内存块的方法在各种Unix版本之间不可移植。而且我们找不到在Windows上创建无名共享内存块的方法。我们发现保证所有访问同一数据库文件的进程使用相同共享内存的唯一方法是,通过将数据库文件所在目录中的文件内存映射来创建共享内存。
使用普通的磁盘文件来提供共享内存的缺点是,它实际上可能会通过将共享内存写入磁盘来执行不必要的磁盘I/O。但是,开发人员认为这不是一个主要问题,因为WAL索引的大小很少超过32 KiB,并且从未同步。此外,WAL索引备份文件在最后一个数据库连接断开时会被删除,这通常可以防止任何实际的磁盘I/O发生。
对于默认共享内存实现不可接受的特殊应用程序可以通过自定义VFS设计替代方法。例如,如果已知特定数据库仅会被单个进程内的线程访问,则可以使用堆内存而不是真正的共享内存来实现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状态。
WAL模式的第二个优点是写入者不会阻塞读取者,读取者也不会阻塞写入者。这大部分是正确的。但是,在某些模糊的情况下,针对WAL模式数据库的查询可能会返回SQLITE_BUSY,因此应用程序应该做好准备以应对这种情况。
针对WAL模式数据库的查询可能返回SQLITE_BUSY的情况包括以下情况
如果另一个数据库连接以独占锁定模式打开数据库模式,则针对该数据库的所有查询都将返回SQLITE_BUSY。例如,Chrome和Firefox都以独占锁定模式打开其数据库文件,因此在应用程序运行时尝试读取Chrome或Firefox数据库将遇到此问题。
当对特定数据库的最后一个连接关闭时,该连接将在清理WAL和共享内存文件时短暂获取独占锁。如果在第一个连接仍在清理过程中时尝试单独打开并查询数据库,则第二个连接可能会收到SQLITE_BUSY错误。
如果对数据库的最后一个连接崩溃了,则打开数据库的第一个新连接将启动恢复过程。在恢复期间会持有独占锁。因此,如果第三个数据库连接试图在第二个连接正在运行恢复时介入并查询,则第三个连接将收到SQLITE_BUSY错误。
对于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