小。速度很快。可靠。
选择任意三个选项。
SQLite中的原子提交

1介绍

SQLite等事务数据库的一个重要特性是“原子提交”。原子提交意味着一个数据库中的所有数据库更改事务发生或全部不发生。使用原子提交,它就好像对数据库的不同部分进行了许多不同的写入文件同时发生。真正的硬件将写操作序列化到大容量存储单个扇区需要有限的时间。因此,不可能真正编写数据库文件同时和/或瞬时。但其中的原子提交逻辑SQLite使其看起来好像事务的更改都是即时同时写的。

SQLite具有事务出现的重要属性即使事务被操作系统崩溃或电源故障。

本文描述了SQLite用于创建原子提交的幻觉。

本文中的信息仅适用于SQLite正在运行的情况在“回滚模式”下,或者换句话说,当SQLite不是使用预写日志.SQLite仍然支持原子提交启用了预写日志记录,但它通过与本文描述的机制不同。请参见这个提前写日志文档有关如何SQLite支持在该上下文中进行原子提交。

2硬件假设

在本文中,我们将大容量存储设备称为“磁盘”即使大容量存储设备可能真的是闪存。

我们假设磁盘是以块的形式写入的,我们称之为“扇区”。无法修改磁盘中小于扇区的任何部分。要更改磁盘中小于扇区的部分,必须读入包含要更改的部分的完整扇区,使更改,然后写出完整的扇区。

在传统的旋转磁盘上,扇区是传输的最小单位在阅读和写作两个方向上。然而,在闪存上,读取的最小大小通常比写入的最小大小小得多。SQLite只关心最小写入量,因此对于本文的目的是,当我们说“部门”时,我们指的是最低金额一次性写入大容量存储器的数据。

在SQLite版本3.3.14之前,512字节的扇区大小为假设在所有情况下。需要更改编译时间选项但代码从未用更大的值进行过测试。这个直到最近,512字节扇区的假设似乎是合理的所有磁盘驱动器内部都使用512字节扇区。然而,这里最近一直在推动将磁盘的扇区大小增加到4096字节。还有扇区大小因为闪存通常大于512字节。基于这些原因,从3.3.14开始的SQLite版本在操作系统中有一个方法查询底层文件系统以查找的接口层真实的扇区大小。当前实施的(版本3.5.0)方法仍然返回512字节的硬编码值,因为并不是发现两个扇区真实大小的标准方法Unix或Windows。但该方法适用于嵌入式设备制造商可以根据自己的需要进行调整。我们有留下了填充更有意义的实现的可能性在Unix和Windows上。

SQLite传统上假设扇区写入原子。然而,SQLite始终假定扇区写入是线性的。按“线性”我们的意思是SQLite假设在写入扇区时,硬件开始在数据的一端,逐字节写入,直到另一端。书写可能从头至尾,也可能从从结束到开始。如果电源故障发生在扇区写入可能是部分扇区被修改另一部分保持不变。SQLite的关键假设如果部门的任何部分发生变化,那么第一个或最后一个字节将被更改。所以硬件会永远不要在中间写一个扇区,朝着末端。我们不知道这个假设是否总是正确的,但它看起来很合理。

上一段指出SQLite并不认为扇区写入是原子的。默认情况下是这样的。但截至SQLite版本3.5.0,有一个新接口,称为虚拟文件系统(变频调速系统)接口。这个变频调速系统是唯一的手段SQLite通过它与底层文件系统通信。这个代码附带了Unix和Windows的默认VFS实现并且有一种机制可以创建新的自定义VFS实现在运行时。在这个新的VFS接口中有一个名为x设备特性。此方法询问底层文件系统来发现文件系统可能显示,也可能不显示。xDeviceCharacteristics方法可能指示扇区写入是原子的,如果是原子的所以请指出,SQLite将尝试利用这一事实。但是Unix和Windows的默认xDeviceCharacteristics方法不指示原子扇区写入,因此这些优化通常省略。

SQLite假设操作系统将缓冲写入和写入请求将在数据实际存储之前返回在大容量存储设备中。SQLite进一步假设写入操作将由操作系统。因此,SQLite在键上执行“flush”或“fsync”操作点。SQLite假定flush或fsync在正在刷新的文件的所有挂起的写入操作都具有完整的。我们被告知flush和fsync原语在某些版本的Windows和Linux上已损坏。这很不幸。它使SQLite面临以下数据库损坏的可能性在提交过程中断电。然而,什么都没有SQLite可以测试或纠正这种情况。数据库假设运行它的操作系统的工作方式为广告。如果情况并非如此,那么希望你不会经常失去权力。

SQLite假设当文件的长度增加时,新的文件空间最初包含垃圾,然后填充实际写入的数据。换句话说,SQLite假定文件大小在文件内容之前更新。这是一个悲观的假设和SQLite必须做一些额外的工作确保在断电时不会导致数据库损坏在文件大小增加和编写新内容。的xDeviceCharacteristics方法这个变频调速系统可能指示文件系统将始终写入更新文件大小之前的数据。(这是用于正在查找的读取器的SQLITE_IOCAP_SAFE_APPEND属性代码。)当xDeviceCharacteristics方法指示文件内容是在文件大小增加之前写入的,SQLite可以放弃一些迂腐的数据库保护步骤从而减少执行提交。然而,当前的实施没有做出这样的假设用于Windows和Unix的默认VFSE。

SQLite假定文件删除是从用户进程的观点。我们的意思是,如果SQLite请求删除文件,并且在删除操作,一旦电源恢复,文件将如果原始内容未被更改,则完全与所有内容一起存在,或否则,文件系统中根本看不到该文件。如果恢复电源后,文件只被部分删除,如果它的一些数据被修改或删除,或文件已被截断但未完全删除,则可能会导致数据库损坏。

SQLite假设宇宙射线、热噪声、量子引起的位误差波动、设备驱动程序错误或其他机制是底层硬件和操作系统的责任。SQLite没有为数据库文件添加任何冗余检测损坏或I/O错误的目的。SQLite假设它读取的数据完全相同它之前写的。

默认情况下,SQLite假定要写入的操作系统调用字节范围不会损坏或更改超出该范围的任何字节即使在写入期间发生断电或操作系统崩溃。我们称之为“电源安全覆盖“属性。之前版本3.7.9(2011-11-01),SQLite没有假定电源安全覆盖。但按照标准在大多数磁盘驱动器上,扇区大小从512字节增加到4096字节为了保持历史性能级别,因此电源安全覆盖假定为SQLite最新版本中的默认值。电源安全的假设如果满足以下条件,则可以在编译时或运行时禁用overwrite属性渴望的。请参阅powersafe覆盖文档进一步细节。

三。单个文件提交

我们首先概述SQLite为实现以下目标所采取的步骤对单个数据库执行事务的原子提交文件。用于防止损坏的文件格式的详细信息电源故障和跨系统执行原子提交的技术后面的部分将讨论多个数据库。

3.1.初始状态

数据库连接为第一次打开的概念图显示在正确的。图中最右边的区域(标记为“磁盘”)表示存储在大容量存储设备上的信息。每个矩形是一个部门。蓝色表示扇区包含原始数据。中间区域是操作系统磁盘缓存。在我们的示例开始时,缓存是冷的,这表示为将磁盘缓存的矩形留空。图的左侧区域显示了的内存内容正在使用SQLite的进程。数据库连接具有刚刚打开,尚未读取任何信息,因此用户空间为空。


3.2.获取读取锁

SQLite必须先读取查看数据库中已有的内容。即使只是附加新数据后,SQLite仍需读取数据库来自“sqlite架构“表以便它可以知道如何解析INSERT语句并发现数据库文件应存储新信息。

读取数据库文件的第一步正在获取数据库文件的共享锁。“共享”lock允许两个或多个数据库连接从数据库文件。但共享锁阻止写入数据库文件的另一个数据库连接这是必要的,因为如果另一个数据库连接正在写入的数据库文件在读取数据库文件的同时,我们可能会读取更改前的一些数据和更改后的其他数据。这将使其看起来好像是另一方所做的更改这个过程不是原子的。

请注意,共享锁位于操作系统上磁盘缓存,而不是磁盘本身。文件锁实际上只是操作系统内核中的标志,通常。(细节取决于特定的操作系统层接口。)因此,如果操作系统崩溃或断电。通常情况下,如果创建锁出口的进程。


3.3.从数据库中读取信息

获取共享锁后,我们可以开始读取来自数据库文件的信息。在这种情况下,我们假设是冷缓存,因此信息必须首先从大容量存储器读取到操作系统缓存中,然后从操作系统缓存传输到用户空间。在随后的阅读中,部分或全部信息可能已在操作系统缓存中找到,因此只有需要转移到用户空间。

通常只有数据库文件中页面的子集已阅读。在这个例子中,我们展示了三个正在阅读八页中的一页。在典型应用中数据库将有数千页,查询通常会只接触这些页面的一小部分。


3.4.获取保留锁

在对数据库进行更改之前,请先使用SQLite获取数据库文件的“保留”锁。A保留锁与共享锁类似,都是保留锁和共享锁允许其他进程从数据库中读取文件。单个保留锁可以与多个共享锁共存来自其他进程的锁。但是,只能有数据库文件上的单个保留锁。因此只有一个单个进程可以尝试写入数据库一次。

保留锁背后的想法是,它表示一个进程打算在但尚未开始进行修改。由于修改尚未开始,其他进程可以继续从数据库中读取。然而,其他进程也不应该开始尝试写入数据库。


3.5.创建回滚日志文件

在对数据库文件进行任何更改之前,请先使用SQLite创建单独的回滚日志文件并写入回滚原始日志要更改的数据库页面的内容。回滚日志背后的思想是它包含将数据库恢复到所需的所有信息其原始状态。

回滚日志包含一个小标题(以绿色显示图中),记录数据库的原始大小文件。因此,如果更改导致数据库文件增长,我们仍将知道数据库的原始大小。页面数字与每个数据库页面一起存储写入回滚日志。

创建新文件时,大多数桌面操作系统(Windows、Linux、Mac OS X)实际上不会向磁盘。新文件在操作系统磁盘中创建仅缓存。直到某个时候,才会在大容量存储上创建文件稍后,当操作系统有空闲时间时。这将创建给用户的印象是I/O的速度比在进行真正的磁盘I/O时是可能的。我们在右边的图表显示了新的回滚日志仅显示在操作系统磁盘缓存中,而不显示在磁盘本身。


3.6.在用户空间中更改数据库页面

在回滚中保存原始页面内容后日志中,可以在用户内存中修改页面。每个数据库connection有自己的用户空间私有副本,因此在用户空间中创建的只对数据库连接可见这就是改变。其他数据库连接仍请参阅操作系统磁盘缓存缓冲区中的信息尚未更改。因此,即使一个进程繁忙修改数据库时,其他进程可以继续读取拥有原始数据库内容的副本。


3.7.将回滚日志文件刷新到大容量存储

下一步是刷新回滚日志的内容文件到非易失性存储器。我们稍后将看到,这是确保数据库能够生存的关键一步意外断电。这一步也需要很多时间,因为写入非易失性存储通常是一个缓慢的操作。

此步骤通常比简单的刷新更复杂回滚日志到磁盘。在大多数平台上,两个独立的需要flush(或fsync())操作。第一次刷新写入导出基本回滚日志内容。然后是回滚日志被修改为显示回滚日志。然后将标头刷新到磁盘。详细信息关于我们为什么要做这个标题修改和额外的刷新在本文的后面部分。


3.8.获取独占锁

在对数据库文件本身进行更改之前,我们必须获取数据库文件的独占锁。获得独占锁实际上是一个两步过程。第一个SQLite获得“挂起”锁。然后将挂起的锁升级为独占锁。

挂起的锁允许已经具有共享锁以继续读取数据库文件。但是它防止建立新的共享锁。这个想法挂起锁的背后是为了防止写入程序饥饿由大量读者提供。可能有几十个,甚至几百个,尝试读取数据库文件的其他进程。每个过程在开始读取之前获取共享锁,读取它的内容需要,然后释放共享锁。然而,如果有许多不同的进程都从同一个数据库中读取可能发生的情况是,新进程总是在之前获取其共享锁前一个进程释放其共享锁。所以就有了数据库上没有共享锁时,决不会有任何瞬间因此,作者永远没有机会抓住专属锁。挂起的锁旨在防止该循环通过允许现有共享锁继续运行,但阻止建立新的共享锁。最终所有共享锁将被清除,挂起的锁将被能够升级为独占锁。


3.9.将更改写入数据库文件

一旦持有独占锁,我们知道没有其他进程正在读取数据库文件,它是可以安全地将更改写入数据库文件。通常这些更改仅限于操作系统磁盘缓存,而不是一直到大容量存储。


3.10.0刷新对大容量存储的更改

必须进行另一次冲洗,以确保数据库更改被写入非易失性存储器。这是确保数据库将在断电后幸存下来而不受伤害。然而,因为写入磁盘或闪存的固有速度慢,此步骤与部分中的回滚日志文件刷新一起执行上面的3.7占用了完成SQLite中的事务提交。


3.11.1删除回滚日志

数据库更改全部安全完成后存储设备,回滚日志文件被删除。这是事务提交的瞬间。如果在此之前发生电源故障或系统崩溃点,然后恢复过程将在后面描述似乎数据库没有任何更改文件。如果之后发生电源故障或系统崩溃回滚日志被删除,然后看起来好像所有更改都已写入磁盘。因此,SQLite给出了未对数据库进行任何更改的外观文件或对数据库文件取决于是否回滚日志文件存在。

删除文件实际上不是一个原子操作,但它似乎是从用户进程的角度来看的。进程总是能够要求操作系统“这个文件存在吗?“该过程将返回“是”或“否”答案。发生电源故障后事务提交,SQLite将询问操作系统回滚日志文件是否存在。如果答案是“是”,则交易不完整回滚。如果答案是“否”,则表示交易确实提交了。

交易的存在取决于回滚日志文件不存在且删除用户空间进程的视图。因此,事务似乎是一个原子操作。

在许多系统上,删除文件的操作代价很高。作为优化,可以将SQLite配置为截断日志文件的长度为零字节或者用零覆盖日志文件头。在任何一种情况下在这种情况下,生成的日志文件不再能够滚动返回,因此事务仍然提交。截断文件零长度,就像删除文件一样,被认为是原子的从用户进程的角度进行操作。覆盖带有零的日志头不是原子的,但如果有部分标头格式错误,日志将不会回滚。因此,可以说提交发生在头已被充分更改,使其无效。通常会发生这种情况只要标头的第一个字节为零。


3.12.2释放锁

提交过程的最后一步是释放独占锁,以便其他进程可以再次开始访问数据库文件。

在右边的图中,我们显示了释放锁时,用户空间中保留的内容将被清除。对于SQLite的旧版本来说,这一点过去是真的。但是最新版本的SQLite保留用户空间信息内存中,以防在开始时再次需要下一个事务。重用已经在本地内存中,而不是将信息传输回从操作系统磁盘缓存或从磁盘驱动器。在重用用户空间中的信息之前,我们必须先重新获取共享锁,然后再进行检查以确保没有其他进程在我们没有拿锁。第一页有一个计数器每次数据库文件增加的数据库已修改。我们可以确定是否有其他进程修改了通过检查计数器来创建数据库。如果数据库被修改,然后必须清除并重新读取用户空间缓存。但确实如此通常情况下,用户没有进行任何更改可以重用空间缓存以显著节省性能。


4.回降

原子提交应该立即发生。但加工上述描述显然需要有限的时间。假设电脑的电源被切断完成上述提交操作的一部分。整齐为了保持这种变化是瞬间发生的错觉,我们必须“回滚”任何部分更改并将数据库恢复到事务开始之前的状态。

4.1.当出现问题时。。。

假设发生断电在期间步骤3.10以上,当数据库更改写入磁盘时。电力恢复后,情况可能会有所不同如右图所示。我们试图改变数据库文件有三页,但只有一页成功写入。另一页已部分写入第三页根本没有写。

电源恢复。这是一个关键点。原因中的刷新操作步骤3.7就是要确保所有回滚日志都安全地存储在非易失性存储器上在对数据库文件本身进行任何更改之前。


4.2.热回滚日志

任何SQLite进程第一次尝试访问数据库文件,它将获得一个共享锁,如中所述第3.2节以上。但随后它注意到有一个存在回滚日志文件。SQLite然后检查是否回滚日志是一个“热日志”。热门期刊是需要回放的回滚日志,以便将数据库恢复到正常状态。仅限热门日志当早期进程处于提交过程中时存在崩溃或断电时的事务。

如果满足以下所有条件,回滚日志就是“热”日志均为真:

热门期刊的出现是我们的迹象前一个进程试图提交事务,但它在完成提交。热门期刊意味着数据库文件处于不一致状态,需要在使用之前修复(通过回滚)。


4.3.获取数据库的独占锁

处理热门期刊的第一步是获取数据库文件的独占锁。这样可以防止两个或多个进程尝试回滚同一热日志同时。


4.4.回滚未完成的更改

一旦进程获得独占锁,就允许使用写入数据库文件。然后继续读取回滚日志中页面的原始内容并写入将内容恢复到数据库文件中的原始位置。回想一下,回滚日志的头记录了原始日志中止开始之前数据库文件的大小交易。SQLite使用此信息截断数据库文件恢复到其原始大小,如果不完整的事务导致数据库增长。此步骤结束时,数据库的大小应该相同,并且包含与开始之前相同的信息中止的事务。


4.5.删除热门日志

回滚日志中的所有信息回放到数据库文件中(并刷新到磁盘以防我们再次遇到电源故障),热回滚日志可以删除。

如中所示第3.11条,日志文件可能被截断为零长度,或者其标头可能用零覆盖作为系统的优化,其中删除文件的成本很高。不管怎样,日记本都不是在这一步之后,热的时间更长。


4.6.继续,就像未完成的写作从未发生过一样

最后一个恢复步骤是减少独占锁定到共享锁。一旦发生这种情况,数据库就会回到声明如果中止的事务从未发生过起动。因为所有这些恢复活动都完全发生了自动透明地显示在程序中SQLite,就像中止的事务从未开始一样。


5多文件提交

SQLite允许单个数据库连接与…交谈通过使用这个附加数据库命令。在单个数据库中修改多个数据库文件时事务,所有文件都会自动更新。换句话说,要么更新所有数据库文件,要么否则他们都不是。跨多个数据库文件实现原子提交是比对单个文件执行此操作更复杂。本节描述了SQLite是如何神奇地工作的。

5.1.每个数据库的单独回滚日志

当事务中涉及多个数据库文件时,每个数据库都有自己的回滚日志和每个数据库已单独锁定。右侧的图表显示了一个场景其中修改了三个不同的数据库文件一次交易。此步骤中的情况类似于位于的单文件事务场景步骤3.6。每个数据库文件都有保留的锁。对于每个数据库,页面的原始内容正在更改的已写入回滚日志对于该数据库,但期刊的内容尚未公布已刷新到磁盘。未对数据库进行任何更改文件本身,尽管可能有更改被保留在用户存储器中。

为了简洁起见,本节中的图表简化为以前的那些。蓝色仍然表示原始内容粉红色仍然代表新的内容。但是单个页面在回滚日志和数据库文件中没有显示,并且我们没有区分操作系统缓存和磁盘上的信息。所有这些因素仍然适用于多文件提交场景只是在图表中占用了大量空间,它们没有添加任何新信息,因此此处省略。


5.2.超级日志文件

多文件提交的下一步是创建“超级日志”文件。超级日志文件的名称为与原始数据库文件名(数据库)同名使用打开的sqlite3_open()接口,没有一个附属的辅助的数据库)与文本“-mj(百万焦耳)HHHHH“附加在此处HHHHH是一个随机的32位十六进制数。这个随机的,随机的HHHHH每个新的超级期刊都会更改后缀。

(注:计算超级日志文件名的公式上一段中给出的对应于实现SQLite版本3.5.0。但这个公式不是SQLite的一部分规范,并可能在未来版本中更改。)

与回滚日志不同,超级日志不包含任何原始数据库页面内容。相反,超级日志包含每个数据库的回滚日志的完整路径名参与交易。

构建超级日志后,将刷新其内容在执行任何进一步操作之前,将其存储到磁盘。在Unix上,目录还同步了包含超级日志的,以确保超级日志文件将出现在电源故障。

超级日志的目的是确保多个文件事务在断电时是原子性的。但如果数据库文件其他设置会影响断电事件的完整性(例如PRAGMA同步=关闭PRAGMA journal_mode=内存)然后作为优化,省略了超级日志的创建。

5.3.更新回滚日志标头

下一步是记录超级日志文件的完整路径名在每个回滚日志的标头中。容纳的空间每个回滚日志的开头都保留了超级日志文件名当创建回滚日志时。

之前,每个回滚日志的内容都会刷新到磁盘并在将超级日志文件名写入回滚之后日记帐标题。这两种刷新都很重要。幸运的是,第二次冲洗通常很便宜,因为通常只有一次日志文件的第页(第一页)已更改。

此步骤类似于步骤3.7在单文件提交中上述场景。


5.4.更新数据库文件

一旦所有回滚日志文件都被刷新到磁盘可以安全地开始更新数据库文件。我们必须获得写入更改之前,对所有数据库文件进行独占锁定。在写入所有更改之后,刷新更改磁盘,以便在发生以下情况时保留这些更改电源故障或操作系统崩溃。

此步骤对应于步骤3.8,3.9、和3.10在单文件提交中前面描述的场景。


5.5.删除超级日志文件

下一步是删除超级日志文件。这是多文件事务提交的点。此步骤对应于步骤3.11在单个文件中删除回滚日志的提交场景。

如果此时发生电源故障或操作系统崩溃点,系统重新启动时事务不会回滚即使存在回滚日志。这个区别在于回滚日志。重新启动时,SQLite只考虑日志成为热点,只有在没有日志时才会播放日志标题中的超级日志文件名(单个文件提交)或如果超级日志文件仍然存在磁盘上存在。


5.6.清理回滚日志

多文件提交的最后一步是删除单个回滚日志并删除上的独占锁数据库文件,以便其他进程可以看到更改。这对应于步骤3.12在单个文件中提交序列。

此时事务已经提交,因此计时在删除回滚日志时并不重要。当前实现删除单个回滚日志然后在继续之前解锁相应的数据库文件到下一个回滚日志。但将来我们可能会改变这样,所有回滚日志都会在任何数据库之前删除文件已解锁。只要之前删除了回滚日志其相应的数据库文件已解锁,这与什么无关删除回滚日志或数据库文件的顺序解锁。

6提交过程的其他详细信息

第3.0节上面概述了SQLite中原子提交的工作原理。但它掩盖了许多重要细节。以下小节将尝试填充在间隙中。

6.1.始终记录完整扇区

将数据库页面的原始内容写入回滚日志(如中所示第3.5节),SQLite总是写入完整的数据扇区,即使数据库的页面大小小于扇区大小。历史上,SQLite中的扇区大小被硬编码为512字节,并且由于最小页面大小也是512字节,因此这从来没有一直是个问题。但从SQLite 3.3.14版开始,这是可能的SQLite使用扇区大小大于512的大容量存储设备字节。因此,从3.3.14版开始,只要扇区写入日志文件,同一扇区中的所有页面与它一起存放。

在回滚中存储扇区的所有页面很重要日志以防止数据库在电源之后损坏写入扇区时丢失。假设第1、2、3和4页全部存储在扇区1中,并且该页2被修改。为了写作对第2页的更改,底层硬件还必须重写第1、3和4页的内容,因为硬件必须编写完整的部门。如果此写入操作因断电而中断,页面1、3或4中的一个或多个可能留下不正确的数据。因此,为了避免对数据库的持久破坏,原始内容所有这些页面中必须包含在回滚日志中。

6.2.处理写入日志文件的垃圾

当数据附加到回滚日志的末尾时,SQLite通常会悲观地假设文件首先使用无效的“垃圾”数据进行扩展,然后再进行扩展正确的数据替换垃圾。换句话说,SQLite假定首先增加文件大小,然后再增加内容写入文件中。如果文件后发生电源故障大小已增加,但在写入文件内容之前,回滚日志可以保留为包含垃圾数据。如果在之后电源恢复后,另一个SQLite进程将看到回滚日志包含垃圾数据并尝试将其回滚到原始数据库文件,它可能会将一些垃圾复制到数据库文件中从而损坏数据库文件。

SQLite针对这个问题使用了两种防御措施。首先,SQLite在页眉中记录回滚日志中的页数回滚日志的。这个数字最初是零。所以在尝试回滚不完整(可能损坏)的回滚日志,执行回滚的进程将看到日志包含零页,因此不会对数据库进行任何更改。之前提交时,回滚日志被刷新到磁盘,以确保所有内容都已同步到磁盘,没有“垃圾”然后,页眉中的页数才从回滚日志中的页数为零到真。回滚日志页眉始终与任何页面数据保持在单独的扇区中,以便它可以被覆盖和刷新,而不会损坏数据如果发生停电,则显示页面。请注意,回滚日志两次刷新到磁盘:一次写入页面内容,另一次在页眉中写入页数的时间。

上一段描述了当同步杂注设置为“full”。

PRAGMA同步=全;

默认同步设置已满,因此通常会出现上述情况发生。然而,如果同步设置降低到“正常”,SQLite只刷新回滚日志一次,在页面计数达到已写入。这有腐败的风险,因为修改后的(非零)页面数首先到达磁盘表面的数据。数据将首先写入,但SQLite假设底层文件系统可以对写请求进行重新排序页数可以首先烧成氧化物,即使写入请求最后发生。作为第二道防线,SQLite还对回滚中的每一页数据使用32位校验和日记账。回滚期间将针对每个页面评估此校验和回滚日志时,如中所述第4.4节。如果校验和不正确可以看到,回滚被放弃。注意校验和不能保证页面数据是正确的,因为但即使数据正确,校验和也可能正确的概率有限腐败的。但校验和至少使这种错误不太可能发生。

请注意,回滚日志中的校验和不是必需的如果同步设置为FULL。我们只依赖校验和当同步降低到NORMAL时。然而,校验和永远不会受伤,因此不管怎样,它们都被包括在回滚日志中同步设置。

6.3.提交前缓存溢出

中显示的提交过程第3.0节假设所有数据库更改都适合内存,直到提交。这是常见的情况。但有时更大的变化会在事务提交之前溢出用户空间缓存。在那些在这种情况下,缓存必须在事务之前溢出到数据库已完成。

在缓存溢出开始时,数据库的状态连接如所示步骤3.6.原始页面内容已保存在回滚日志中,并且页面的修改存在于用户内存中。要溢出缓存,SQLite执行步骤3.7通过3.9换句话说,回滚日志刷新到磁盘,获取独占锁,更改写入数据库。但其余步骤被推迟直到事务真正提交。新的日记帐标题是附加到回滚日志的末尾(在其自己的扇区中)并保留独占数据库锁,否则进行处理返回到步骤3.6.当交易提交,或者如果发生另一个缓存溢出,则执行步骤3.73.9重复的。(步骤3.8第二个被省略和后续传递,因为已持有独占数据库锁由于第一次通过。)

缓存溢出导致数据库文件上的锁定从保留升级到独占。这会降低并发性。缓存溢出还会导致额外的磁盘刷新或fsync操作发生并且这些操作很慢,因此缓存溢出可能严重降低性能。由于这些原因,尽可能避免缓存溢出。

7优化

分析表明,对于大多数系统和大多数情况下SQLite的大部分时间都花在磁盘I/O上为了减少磁盘I/O量,我们所能做的任何事情都可能会对SQLite的性能有很大的积极影响。本节描述了SQLite用来减少将磁盘I/O量降至最低,同时仍保留原子提交。

7.1.事务之间保留的缓存

步骤3.12提交过程的一旦释放共享锁,所有用户空间缓存必须丢弃数据库内容的图像。这样做是因为如果没有共享锁,其他进程可以自由修改数据库文件内容,因此该内容的任何用户空间图像都可能成为过时的。因此,每个新事务都将从重新读取开始以前读取过的数据。这并不像听起来那么糟糕首先,因为正在读取的数据可能仍在运行中系统文件缓存。所以“读取”实际上只是数据的拷贝从内核空间到用户空间。但即便如此,这仍然需要时间。

从SQLite 3.3.14版开始,添加了一种机制试图减少不必要的数据重读。在较新版本中对于SQLite,当数据库文件上的锁被释放。稍后,在共享锁在下一个事务开始时获取,SQLite检查是否有其他进程修改了数据库文件。如果自锁定后数据库已以任何方式更改上一次发布时,用户空间缓存将被擦除。但通常数据库文件保持不变,用户空间缓存可以保留,并且可以避免一些不必要的读取操作。

为了确定数据库文件是否已更改,SQLite在数据库头中使用计数器(以字节24到27为单位)其在每次改变操作期间递增。SQLite保存副本在释放其数据库锁之前。之后获取下一个数据库锁-比较保存的计数器值根据当前计数器值,如果值不同,或者如果缓存相同,则重用缓存。

7.2.独占访问模式

SQLite版本3.3.14添加了“独占访问模式”的概念。在独占访问模式下,SQLite保留独占在每个事务结束时锁定数据库。这样可以防止访问数据库的其他进程,但在许多部署中只有一个进程正在使用数据库,因此这不是严重问题。独占访问模式的优点是可以通过三种方式减少磁盘I/O:

  1. 无需在第一个事务之后的事务的数据库头。这个通常会将第一页的写入保存到两个回滚日志和主数据库文件。

  2. 没有其他进程可以更改数据库,因此永远不会需要检查更改计数器并清除用户空间缓存在事务开始时。

  3. 可以通过覆盖回滚来提交每个事务带有零的日志头,而不是删除日志文件。这样可以避免修改日志文件的目录条目并且它避免了必须取消分配与日记账。此外,下一个事务将覆盖现有的日志文件内容,而不是在大多数系统上附加新内容覆盖比追加快得多。

第三个优化是将日志文件头归零,而不是删除回滚日志文件,不依赖于始终持有独占锁。此优化可以独立于独占锁定模式进行设置使用日志模式杂注如中所述第7.6节如下所示。

7.3.不记录自由列表页面

从SQLite数据库中删除信息时,使用的页面将删除的信息添加到自由名单“.后续插入将从这个自由列表中抽出页面,而不是展开数据库文件。

一些免费页面包含关键数据;特别是地点其他免费页面的。但大多数免费页面都没有任何有用的内容。这些后面的免费页面称为“叶子”页面。我们可以自由在数据库中修改叶自由列表页面的内容以任何方式更改数据库的含义。

由于叶自由列表页面的内容并不重要,SQLite避免在回滚日志中存储叶空闲列表页面内容在里面步骤3.5提交过程的。如果叶自由列表页面被更改,并且该更改没有回滚在事务恢复期间,数据库不会因遗漏而受到损害。同样,新的免费列表页面的内容也不会被写回到数据库中步骤3.9也不是从位于的数据库中读取步骤3.3.这些优化可以大大减少发生的I/O量对包含可用空间的数据库文件进行更改时。

7.4.单页更新和原子扇区写入

从SQLite版本3.5.0开始,新的虚拟文件系统(VFS)接口包含一个名为xDeviceCharacteristics的方法,该方法报告关于底层大容量存储设备的特殊特性可能有。在这些特殊属性中xDeviceCharacteristics可能报告的是原子扇区写入。

回想一下,默认情况下,SQLite假设扇区写入线性的,但不是原子的。线性写入从扇区并逐字节更改信息,直到到达行业的另一端。如果在可以修改扇区的一部分线性写入,而另一端不变。在原子扇区中写入扇区被覆盖,否则扇区中的任何内容都不会更改。

我们相信大多数现代磁盘驱动器都实现了原子扇区写入。断电时,驱动器使用电容器中存储的能量和/或磁盘盘的角动量为完成任何正在进行的操作。然而,有这么多写系统调用和板载磁盘驱动器之间的层我们在Unix和w32 VFS中采用安全方法的电子设备实现并假设扇区写入不是原子的。另一方面,设备对文件系统拥有更多控制权的制造商可能希望考虑启用xDeviceCharacteristics的原子写入属性如果他们的硬件真的可以进行原子写入。

当扇区写入是原子的且数据库的页面大小为与扇区大小相同,并且当数据库发生更改时只接触一个数据库页面,然后SQLite跳过整个页面日志记录和同步过程,只需写入修改过的页面直接写入数据库文件。第一个数据库文件的页面被单独修改,因为不会造成任何危害如果在更改计数器更新之前断电,则执行此操作。

7.5.具有安全附加语义的文件系统

SQLite版本3.5.0中引入的另一个优化使使用基础磁盘的“安全附加”行为。回想一下,SQLite假设当数据附加到文件时(特别是回滚日志)文件的大小首先增加内容,然后写入内容。所以如果在文件大小增加后但在写入内容时,文件仍包含无效的“垃圾”数据。然而,VFS的xDeviceCharacteristics方法可能,指示文件系统实现“安全附加”语义。这意味着在文件大小为增加,从而不可能引入垃圾由于断电或系统崩溃而进入回滚日志。

当为文件系统指示了安全附加语义时,SQLite总是为页面计数存储特殊值-1在回滚日志的标头中。-1页计数值告诉任何试图回滚日志的进程日志中的页数应根据日志计算大小。此-1值从未更改。因此,当提交发生时,我们保存一次刷新操作和日志文件的第一页。此外,当缓存发生溢出时,我们不再需要附加新的日志标题到期刊末尾;我们可以简单地继续追加现有期刊末尾的新页面。

7.6.持久回滚日志

在许多系统上,删除文件是一项昂贵的操作。因此,作为优化,可以配置SQLite以避免的删除操作第3.11条.不是为了提交事务而删除日志文件,文件长度被截断为零字节或标头被零覆盖。将文件截断为零长度节省了对包含因为文件没有从目录中删除。覆盖页眉可以节省额外的开销更新文件的长度(在许多系统的“inode”中)而且不必处理新释放的磁盘扇区。此外,在下一个事务中,将通过覆盖来创建日记帐现有内容,而不是将新内容附加到末尾覆盖通常比追加快得多。

可以将SQLite配置为通过覆盖来提交事务日志标头为零,而不是删除日志文件通过使用日志模式普拉格玛。例如:

PRAGMA journal_mode=PERSIST;

持久日志模式的使用提供了显著的性能许多系统的改进。当然,缺点是日志文件保留在磁盘上,占用磁盘空间并造成混乱目录,事务提交后很长时间。唯一安全的方法删除持久日志文件就是提交事务日志模式设置为DELETE时:

PRAGMA journal_mode=删除;开始排除;承诺;

注意不要通过任何其他方式删除永久性日志文件因为日志文件可能是热的,在这种情况下删除它会损坏相应的数据库文件。

从SQLite开始版本3.6.4(2008-10-15),TRUNCATE日志模式为还支持:

PRAGMA journal_mode=TRUNCATE;

在截断日志模式下,通过截断提交事务日志文件的长度为零,而不是删除日志文件(如DELETE模式)或将标头归零(如PERSIST模式)。TRUNCATE模式分享了PERSIST模式的优点,即目录包含日志文件和数据库的,不需要更新。因此,截断文件通常比删除文件快它的另一个优点是没有系统调用(ex:fsync())将更改同步到磁盘。它可能会如果是这样的话就更安全了。但在许多现代文件系统上,truncate是一个原子和同步操作,因此我们认为TRUNCATE通常是安全的面对停电。如果您不确定是否或不是TRUNCATE将在您的文件系统上是同步的和原子的,它是数据库在断电或运行时仍能幸存下来,这对您来说很重要在截断操作期间发生的系统崩溃,那么您可能会考虑使用不同的日志模式。

在具有同步文件系统的嵌入式系统上,TRUNCATE结果行为比PERSIST慢。提交操作的速度相同。但在执行TRUNCATE之后,后续事务的速度较慢,因为它是覆盖现有内容比附加到文件末尾更快。新日志文件条目将始终附加在TRUNCATE之后,但通常会用PERSIST覆盖。

8测试原子提交行为

SQLite的开发人员相信它是健壮的面对电源故障和系统崩溃自动测试程序对SQLite从模拟功率损失中恢复的能力。我们称之为“碰撞测试”。

SQLite中的崩溃测试使用可以模拟的修改过的VFS在通电期间发生的文件系统损坏类型丢失或操作系统崩溃。碰撞测试VFS可以模拟扇区写入不完整,页面充满垃圾数据,因为写操作尚未完成,写操作无序在测试场景中的不同点。执行崩溃测试一次又一次的事务,改变了模拟功率损失的发生和造成的财产损失。每次测试都会在模拟崩溃后重新打开数据库验证事务是否完全发生或者根本没有,数据库完全位于状态一致。

SQLite的崩溃测试发现恢复机制中的细微错误(现已修复)。一些这些错误非常模糊,不太可能被发现仅使用代码检查和分析技术。从这里经验,SQLite的开发人员相信不使用类似碰撞测试系统的数据库系统可能包含未检测到的错误,这些错误将导致数据库系统崩溃或电源故障后的损坏。

9.可能出错的事情

SQLite中的原子提交机制已被证明是健壮的,但它可以通过一个足够有创意的对手或操作系统实现完全失败。本节介绍SQLite数据库的几种方法可能因电源故障或系统崩溃而损坏。(另请参见:如何损坏数据库文件.)

9.1.锁定装置损坏

SQLite使用文件系统锁来确保只有一个进程和数据库连接正在尝试修改数据库一次。实现了文件系统锁定机制在VFS层中,每个操作系统都不同。SQLite依赖于此实现是否正确。如果有什么事出现错误,两个或多个进程能够写入相同的内容同时,可能会导致数据库文件严重损坏。

我们已经收到了这两项技术的实施报告锁定的Windows网络文件系统和NFS轻微断裂。我们无法核实这些报告,但作为在网络文件系统上很难正确锁定我们没有理由怀疑他们。建议您首先避免在网络文件系统上使用SQLite,因为性能会很慢。但如果您必须使用存储SQLite数据库文件的网络文件系统,请考虑使用辅助锁止机构防止同时写入同一数据库,即使本机文件系统锁止机构故障。

苹果上预装的SQLite版本Mac OS X计算机包含的SQLite版本扩展为使用在苹果支持的所有网络文件系统。这些扩展只要所有进程都在访问,苹果使用的就很好以相同的方式保存数据库文件。不幸的是,锁定机制不会相互排斥,因此如果一个进程使用(例如)AFP锁定和其他方式访问文件进程(可能在不同的机器上)使用点文件锁,这两个进程可能会冲突,因为AFP锁不排除点文件锁或反之亦然。

9.2.不完整的磁盘刷新

SQLite在Unix和FlushFileBuffers()上使用fsync()系统调用对w32进行系统调用,以便将文件系统缓冲区同步到磁盘上氧化物,如所示步骤3.7步骤3.10。很遗憾,我们收到了报告说,这两个接口都没有在许多系统。我们听说FlushFileBuffers()可以完全禁用在某些Windows版本上使用注册表设置。一些历史Linux版本包含的fsync()版本是no-ops on我们被告知有些文件系统。即使在系统上FlushFileBuffers()和fsync()通常可以正常工作IDE磁盘控件撒谎并表示数据已达到氧化状态而它仍然只保存在volatile控制缓存中。

在Mac上,您可以设置以下杂注:

PRAGMA fullfsync=开;

在Mac上设置fullfsync将确保数据真正做到冲水时被推到磁盘盘中。但是实施fullfsync包括重置磁盘控制器。不仅如此它是否非常慢,也会减慢其他无关的磁盘I/O。因此不建议使用。

9.3.部分文件删除

SQLite假定文件删除是来自用户进程的观点。如果电源在文件删除,然后在电源恢复后,SQLite希望看到要么完整保存所有原始数据的整个文件,要么预计根本找不到该文件。事务可能不是原子事务在不以这种方式工作的系统上。

9.4.垃圾写入文件

SQLite数据库文件是普通的磁盘文件,可以由普通用户进程打开和编写。一个无赖的过程可以打开SQLite数据库并用损坏的数据填充它。损坏的数据也可能被引入SQLite数据库操作系统或磁盘控制器中的错误;尤其地电源故障引发的错误。SQLite什么都不能这样做是为了防范这些问题。

9.5.删除或重命名热门日志

如果发生崩溃或断电,并且热日志仍处于打开状态磁盘,原始数据库文件和日志保留在磁盘上,并保留其原始名称,直到数据库文件由另一个SQLite进程打开并回滚。恢复期间步骤4.2SQLite位于通过在与数据库的名称派生自正在打开的文件。如果原始数据库文件或已移动或重命名热日志,则该热日志将不可见,数据库也不会回滚。

我们怀疑发生了SQLite恢复的常见故障模式例如:发生电源故障。电力恢复后用户或系统管理员开始在磁盘上查找损坏。他们看到了名为“important.data”的数据库文件。此文件也许他们很熟悉。但在坠毁之后,还有一个热门期刊命名为“重要数据期刊”。然后用户删除热门杂志,认为他们正在帮助清理系统。我们知道除了用户教育之外,没有其他方法可以防止这种情况发生。

如果一个数据库文件有多个链接(硬链接或符号链接),日志将使用链接的名称创建,通过该链接文件已打开。如果发生崩溃并再次打开数据库使用不同的链接,将无法定位热门日志将发生回滚。

有时电源故障会导致文件系统损坏这样就忘记了最近更改的文件名移动到“丢失+找到”目录中。当这种情况发生时找不到日志,也不会进行恢复。SQLite试图阻止这种情况通过打开并同步包含回滚日志的目录同时同步日志文件本身。然而文件移入/丢失+找到可能是由不相关的进程引起的在与主数据库文件相同的目录中创建不相关的文件。由于这不在SQLite的控制之下,所以什么都没有SQLite可以做些什么来防止它。如果你在一个易受这种文件系统名称空间损坏(大多数我们相信,现代日志文件系统是免疫的)想考虑将每个SQLite数据库文件放在自己的私有文件中子目录。

10.未来方向和结论

不时有人发现新的故障模式SQLite中的原子提交机制,开发人员必须贴上补丁。这种情况越来越少失效模式变得越来越模糊。但它会仍然愚蠢地认为SQLite完全没有错误。开发人员致力于修复这些错误会尽快被发现。

开发人员也在寻找新的方法来优化提交机制。当前的VFS实现对于Unix(Linux和Mac OS X)和Windows这些系统的行为。与专家协商后关于这些系统是如何工作的,我们可以放松一些对这些系统进行假设,并使其运行得更快。特别是,我们怀疑大多数现代文件系统都会显示安全的append属性,其中许多可能支持atomic扇区写入。但在这一点明确之前,SQLite将采取保守的方法,假设最坏的情况。

此页面上次修改时间2022-12-31 21:51:03联合技术公司