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-index”保留在共享内存中,这有助于读者以最少的I / O迅速在WAL中定位页面。wal-index极大地提高了读取器的性能,但是共享内存的使用意味着所有读取器必须存在于同一台计算机上。
编写者仅将新内容附加到WAL文件的末尾。因为作家不做任何会干扰读者行为的事情,所以作家和读者可以同时运行。但是,由于只有一个WAL文件,所以一次只能有一个写入器。
检查点操作从WAL文件中获取内容,并将其传输回原始数据库文件中。检查点可以与阅读器同时运行,但是当检查点到达WAL中的页面超过任何当前阅读器的结束标记时,必须停止。检查点必须在该点处停止,因为否则它可能会覆盖读取器正在使用的部分数据库文件。该检查点会记住(在wal-index中)到达的距离,并将继续将内容从WAL传输到数据库,该内容从下一次调用停止的位置开始传输。
因此,长时间运行的读取事务可能会阻止检查指针取得进展。但是大概每个读取事务最终都会结束,并且检查指针将能够继续。
每当发生写操作时,编写器都会检查检查指针取得了多少进展,并且如果整个WAL已传输到数据库并进行了同步,并且如果没有读取器正在使用WAL,则编写器会将WAL倒带回至开始,并在WAL的开始处开始进行新的交易。这种机制可以防止WAL文件无限制地增长。
写事务非常快,因为它们只涉及一次写入内容(对于回滚日志事务而言则是两次),并且由于写入都是顺序的。此外,只要应用程序愿意在断电或硬重启后牺牲耐用性,就不需要将内容同步到磁盘。(如果将PRAGMA同步设置为FULL,则写入程序将在每次事务提交时同步WAL,但如果将PRAGMA同步设置为NORMAL,则编写者将 忽略此同步 。)
另一方面,随着WAL文件大小的增加,读取性能也会下降,因为每个阅读器都必须检查WAL文件中的内容,并且检查WAL文件所需的时间与WAL文件的大小成正比。wal-index有助于更快地在WAL文件中查找内容,但是性能随着WAL文件大小的增加而下降。因此,为了保持良好的读取性能,重要的是通过定期运行检查点来减小WAL文件的大小。
检查点确实需要同步操作,以避免断电或硬重启后数据库损坏的可能性。在将内容从WAL移入数据库之前,必须将WAL同步到持久性存储,并且在重置WAL之前必须同步数据库文件。Checkpoint还需要更多寻求。检查指针会尽一切努力对数据库进行尽可能多的顺序页面写入(页面以升序从WAL传输到数据库),但是即使那样,页面写入之间通常也会散布着许多查找操作。这些因素共同导致检查点的速度比写入事务的速度慢。
默认策略是允许连续的写事务增长WAL,直到WAL变为大约1000页,然后对每个后续COMMIT运行检查点操作,直到WAL重置为小于1000页为止。默认情况下,检查点将由执行COMMIT的同一线程自动运行,该线程使WAL超过其大小限制。这会导致大多数COMMIT操作非常快,但偶尔的COMMIT(触发检查点的操作)会慢得多。如果不希望这种影响,则应用程序可以禁用自动检查点,并在单独的线程或单独的进程中运行定期检查点。(实现此目的的命令和界面的链接 如下所示。)
请注意,在PRAGMA同步设置为NORMAL的情况下,检查点是发出I / O屏障或同步操作的唯一操作(unix上的fsync()或Windows上的FlushFileBuffers())。因此,如果应用程序在单独的线程或进程中运行检查点,则执行数据库查询和更新的主线程或进程将永远不会阻塞同步操作。这有助于防止在繁忙的磁盘驱动器上运行的应用程序中出现“闩锁”。此配置的缺点是事务不再持久,并且可能在电源故障或硬重置后回滚。
还要注意,在平均读取性能和平均写入性能之间需要权衡。为了最大程度地提高读取性能,人们希望使WAL尽可能小,并因此频繁地运行检查点,也许与每个COMMIT一样频繁。为了最大程度地提高写性能,人们希望在尽可能多的写操作中分摊每个检查点的成本,这意味着人们希望不频繁地运行检查点,并让WAL在每个检查点之前尽可能地大。因此,取决于应用程序的相对读写性能要求,运行检查点的频率的决定可能因一个应用程序而异。默认策略是在WAL达到1000页后运行检查点,并且该策略在工作站上的测试应用程序中似乎效果很好,
SQLite数据库连接默认为 journal_mode = DELETE。若要转换为WAL模式,请使用以下编译指示:
PRAGMA journal_mode = WAL;
journal_mode编译指示返回一个字符串,它是新的日志模式。成功时,编译指示将返回字符串“ wal ”。如果无法完成向WAL的转换(例如,如果VFS 不支持必要的共享内存原语),则日记记录模式将保持不变,并且从原语返回的字符串将是先前的日记记录模式(例如“删除”)。
默认情况下,只要发生COMMIT 导致WAL文件的大小为1000页或更多,或者关闭数据库文件上的最后一个数据库连接,SQLite就会自动检查点。默认配置适用于大多数应用程序。但是,需要更多控制的程序可以使用wal_checkpoint编译指示或调用 sqlite3_wal_checkpoint() C接口来强制执行检查点。可以使用wal_autocheckpoint编译指示或调用 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,它会在不干扰其他数据库连接的情况下尽其所能,并且如果存在并发的读取器或写入器,则可能无法完成。由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将应用程序转换为应用程序,而无需对应用程序本身进行任何更改。只需使用命令行Shell或其他实用程序在数据库文件上运行“ PRAGMA journal_mode = WAL; ” ,然后重新启动应用程序。
如果在任何一个连接上都设置了WAL日志模式,则将在到同一数据库文件的所有连接上设置它。
在WAL模式数据库上打开数据库连接时,SQLite会维护一个额外的日志文件,称为“预写日志”或“ WAL文件”。磁盘上此文件的名称通常是后缀为-wal的数据库文件的名称,但是如果使用SQLITE_ENABLE_8_3_NAMES编译SQLite,则可能会应用不同的命名规则。
只要任何数据库连接都打开了数据库,WAL文件就存在。通常,与数据库的最后一个连接关闭时,将自动删除WAL文件。但是,如果退出数据库的最后一个进程退出而没有干净地关闭数据库连接,或者 SQLITE_FCNTL_PERSIST_WAL 文件控件使用WAL,则在关闭与数据库的所有连接之后,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杂注)。相反,它仅使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文件。
在沃尔玛指数是否使用了mmapped健壮性一个普通文件实现。WAL模式的早期(预发行版)实现将wal-index存储在易失的共享内存中,例如在Linux上的/ dev / shm中创建的文件,或在其他unix系统上的/ tmp中创建的文件。这种方法的问题在于,进程具有不同的根目录(通过chroot进行了更改))将看到不同的文件,因此使用不同的共享内存区域,从而导致数据库损坏。创建无名共享内存块的其他方法不能跨各种unix移植。而且我们找不到在Windows上创建无名共享内存块的任何方法。我们发现保证访问同一数据库文件的所有进程使用相同共享内存的唯一方法是通过映射与数据库本身相同的目录中的文件来创建共享内存。
使用普通磁盘文件提供共享内存的缺点是,通过将共享内存写入磁盘,它实际上可能会执行不必要的磁盘I / O。但是,开发人员认为这不是主要问题,因为wal-index的大小很少会超过32 KiB,并且永远不会同步。此外,当最后一个数据库连接断开时,wal-index支持文件也将被删除,这通常可以防止发生任何实际的磁盘I / O。
无法接受共享内存的默认实现的特殊应用程序可以通过自定义VFS设计替代方法。例如,如果已知特定数据库仅由单个进程中的线程访问,则可以使用堆内存而不是真正的共享内存来实现wal-index。
从SQLite版本3.7.4(2010-12-07)开始,即使共享内存不可用,只要在首次尝试访问之前将locking_mode设置为EXCLUSIVE ,就可以创建,读取和写入WAL数据库 。换句话说,如果保证该进程是访问数据库的唯一进程,则该进程可以与WAL数据库进行交互而无需使用共享内存。此功能允许在sqlite3_io_methods对象上缺少“版本2”共享内存方法xShmMap,xShmLock,xShmBarrier和xShmUnmap 的旧版VFS创建,读取和写入WAL数据库。
如果 在第一次WAL模式数据库访问之前设置了EXCLUSIVE锁定模式,则SQLite绝不会尝试调用任何共享内存方法,因此将永远不会创建共享内存wal-index。在这种情况下,只要日志记录模式为WAL,数据库连接就保持为EXCLUSIVE模式。尝试使用“ PRAGMAlocking_mode = NORMAL; ”更改锁定模式是无操作。退出EXCLUSIVE锁定模式的唯一方法是首先退出WAL日志模式。
如果“普通”锁定模式对第一次WAL模式数据库访问有效,那么将创建共享内存wal-index。这意味着基础VFS必须支持“版本2”共享内存。如果VFS不支持共享内存方法,则尝试打开已经处于WAL模式的数据库或将数据库转换为WAL模式的尝试将失败。只要一个连接使用共享内存的wal-index,锁定模式就可以在NORMAL和EXCLUSIVE之间自由更改。仅当省略共享内存wal-index且锁定模式在第一次WAL模式数据库访问之前为EXCLUSIVE时,锁定模式才会停留在EXCLUSIVE中。
WAL模式的第二个优点是,编写者不会阻止读者,而阅读者也不会阻止作家。这基本上是对的。但是在某些模糊的情况下,针对WAL模式数据库的查询可以返回SQLITE_BUSY,因此应为这种情况做好应用程序的准备。
针对WAL模式数据库的查询可以返回SQLITE_BUSY的情况 包括:
如果另一个数据库连接以独占锁定模式打开了数据库模式,则对数据库的所有查询都将返回SQLITE_BUSY。Chrome和Firefox均以互斥锁定模式打开其数据库文件,因此,例如,在应用程序运行时尝试读取Chrome或Firefox数据库将遇到此问题。
当与特定数据库的最后一个连接关闭时,该连接将在清除WAL和共享内存文件的同时获得一小段排他锁。如果在第一个连接仍处于其清理过程的中间时,另一个数据库尝试打开并查询该数据库,则第二个连接可能会收到SQLITE_BUSY 错误。
如果与数据库的最后一个连接崩溃,则第一个打开数据库的新连接将启动恢复过程。恢复期间将保留排他锁。因此,如果在第二个连接正在运行恢复的同时第三个数据库连接尝试跳入并查询,则第三个连接将收到SQLITE_BUSY错误。
对于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 =已删除;
故意从WAL模式更改为将数据库文件格式版本号改回1,以便旧版本的SQLite可以再次访问数据库文件。