返回:SQLite—系列文章目录
上一篇:SQLite数据库成为内存中数据库(三)
下一篇: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不假设 扇区写入是原子的。默认情况下,这是 true 的。但截至 SQLite 版本 3.5.0,有一个新接口称为 虚拟文件系统 (VFS) 接口。VFS是唯一的手段 SQLite通过它与底层文件系统进行通信。这 代码附带了适用于 Unix 和 Windows 的默认 VFS 实现 并且有一种用于创建新的自定义 VFS 实现的机制 在运行时。在这个新的 VFS 接口中,有一个称为 xDeviceCharacteristics。此方法询问基础 文件系统来发现各种属性和行为 文件系统可能会也可能不会出现。xDeviceCharacteristics 方法可能指示扇区写入是原子的,如果是 所以表明,SQLite将尝试利用这一事实。但 Unix 和 Windows 的默认 xDeviceCharacteristics 方法 不表示原子扇区写入,因此这些优化 通常被省略。
SQLite 假设操作系统将缓冲写入和 写入请求将在实际存储数据之前返回 在大容量存储设备中。 SQLite 进一步假设写入操作将按 操作系统。 出于这个原因,SQLite在键上执行“flush”或“fsync”操作 点。SQLite 假设 flush 或 fsync 不会返回,直到 对于正在刷新的文件,所有挂起的写入操作都具有 完成。我们被告知 flush 和 fsync 原语 在某些版本的 Windows 和 Linux 上已损坏。这是不幸的。 它使SQLite面临以下数据库损坏的可能性 提交过程中的断电。但是,什么都没有 SQLite可以做来测试或补救这种情况。SQLite的 假定运行它的操作系统以 广告。如果情况并非如此,那么希望您 不会经常断电。
SQLite 假设当文件长度增长时,新的 文件空间最初包含垃圾,后来被填充 实际写入数据。换句话说,SQLite假设 文件大小在文件内容之前更新。这是一个 悲观的假设和SQLite必须做一些额外的工作才能使 确保在断电时不会导致数据库损坏 在文件大小增加的时间和 编写新内容。的 xDeviceCharacteristics 方法 VFS 可能指示文件系统将始终写入 数据,然后再更新文件大小。(这是 为那些正在寻找的读者提供SQLITE_IOCAP_SAFE_APPEND属性 在代码中。当 xDeviceCharacteristics 方法指示 文件内容是在文件大小增加之前写入的, SQLite可以放弃一些迂腐的数据库保护步骤 从而减少执行 犯。然而,目前的实现没有做出这样的假设 用于 Windows 和 Unix 的默认 VFS。
SQLite 假设文件删除是原子的用户进程的观点。我们的意思是,如果SQLite 请求删除文件并在 删除操作,一旦电源恢复,文件将如果其原始内容未改变,则完全存在,或者否则,该文件将根本不会在文件系统中看到。如果电源恢复后,文件仅部分删除, 如果其某些数据已被更改或删除, 或者文件已被截断但未完全删除,则可能会导致数据库损坏。
SQLite 假设检测和/或纠正 由宇宙射线、热噪声、量子引起的位误差 波动、设备驱动程序错误或其他机制是底层硬件和操作系统的责任。 SQLite不会向数据库文件添加任何冗余检测损坏或 I/O 错误的目的。 SQLite假设它读取的数据与数据完全相同它之前写过。
默认情况下,SQLite 假定操作系统调用要写入字节范围不会损坏或更改该范围之外的任何字节 即使在写入过程中发生断电或操作系统崩溃。我们 将此称为“PowerSafe Overwrite”属性。 在版本 3.7.9 (2011-11-01) 之前,SQLite 没有假设 powersafe覆盖。但是有了标准在大多数磁盘驱动器上,扇区大小从 512 字节增加到 4096 字节,它 为了维护 历史性能水平,因此 powersafe 覆盖由 在最新版本的 SQLite 中是默认的。powersafe 的假设 如果出现以下情况,可以在编译时或运行时禁用 overwrite 属性 期望。有关更多信息,请参阅 powersafe 覆盖文档 详。
3. 单文件提交
我们首先概述SQLite采取的步骤,以便对单个数据库执行事务的原子提交文件。用于防止损坏的文件格式的详细信息 电源故障和执行原子提交的技术多个数据库将在后面的章节中讨论。
3.1. 初始状态
数据库连接时计算机的状态首次打开在概念上显示在 右。 最右边的图表区域(标记为“磁盘”)代表存储在大容量存储设备上的信息。每个矩形是 一个部门。蓝色表示扇区包含原始数据。 中间区域是操作系统磁盘缓存。在 在我们的示例开始时,缓存是冷的,这表示为通过将磁盘缓存的矩形留空。 图的左侧区域显示了使用 SQLite 的进程。数据库连接具有刚刚打开,还没有读取任何信息,所以 用户空间为空。
3.2. 获取读锁
在SQLite可以写入数据库之前,它必须首先读取 数据库来查看已经存在的内容。即使它只是 附加新数据,SQLite仍然需要读取数据库 schema from the “sqlite_schema” table 以便它可以知道 如何解析 INSERT 语句并发现 数据库文件,应存储新信息。
从数据库文件读取的第一步 正在获取数据库文件的共享锁。一个“共享” lock 允许两个或多个数据库连接从 数据库文件。但是共享锁可以防止 写入数据库文件的另一个数据库连接当我们阅读它时。这是必要的,因为如果另一个 数据库连接正在写入数据库文件,位于当我们从数据库文件读取时,我们可能会读取 更改前的一些数据和更改后的其他数据。 这将使它看起来好像是另一个人所做的更改 过程不是原子的。
请注意,共享锁位于操作系统上 磁盘缓存,而不是磁盘本身。文件锁 真的只是操作系统内核中的标志,通常。(详细信息取决于特定的操作系统层接口。因此,如果操作系统崩溃或断电。它通常也是这样一种情况,如果创建锁的进程退出。
3.3. 从数据库中读取信息
获取共享锁后,我们可以开始阅读 数据库文件中的信息。在这种情况下,我们假设是冷缓存,所以信息必须首先 然后从大容量存储读取到操作系统缓存中从操作系统缓存传输到用户空间。 在随后的读取中,部分或全部信息可能 已经在操作系统缓存中找到,所以只有需要转移到用户空间。
通常只有数据库文件中页面的子集 被读取。在此示例中,我们展示了三个 正在阅读的八页。在典型应用中, 数据库将有数千个页面,查询通常会只接触这些页面的一小部分。
3.4. 获取保留锁
在对数据库进行更改之前,首先要 SQLite 获取数据库文件的“保留”锁。保留锁与共享锁类似,因为两者都是保留锁 和共享锁允许其他进程从数据库中读取数据 文件。单个备用锁可以与多个共享锁共存来自其他进程的锁。但是,只能有一个 数据库文件上的单个保留锁。因此,只有单个进程可能正在尝试写入数据库 一次。
保留锁背后的想法是它发出信号 进程打算在 NEAR 中修改数据库文件 未来,但尚未开始进行修改。 而且由于修改尚未开始,其他 进程可以继续从数据库中读取数据。然而 任何其他进程也不应开始尝试写入 数据库。
3.5. 创建回滚日志文件
在对数据库文件进行任何更改之前,SQLite 首先创建一个单独的回滚日志文件,并写入 回滚日志原始日志 要更改的数据库页的内容。 回滚日志背后的想法是它包含将数据库还原回其原始状态。
回滚日志包含一个小标题(显示为绿色 在图中),记录数据库的原始大小 文件。因此,如果更改导致数据库文件增长,我们 仍将知道数据库的原始大小。页面 数字与每个数据库页一起存储,即写入回滚日志。
创建新文件时,大多数桌面操作系统 (Windows、Linux、Mac OS X)实际上不会写入任何内容 磁盘。新文件是在操作系统磁盘中创建的 仅限缓存。直到某个时候,该文件才会在大容量存储上创建 稍后,当操作系统有空闲时间时。这将创建 给用户的印象是 I/O 的发生速度比 在执行真实磁盘 I/O 时是可能的。我们在 右边的图显示了新的回滚日志仅出现在操作系统磁盘缓存中,而不在磁盘本身。
3.6. 更改用户空间中的数据库页面
在回滚中保存原始页面内容后 日志,页面可以在用户内存中修改。每个数据库 连接有自己的用户空间的私有副本,因此更改 在用户空间中创建的内容仅对数据库连接可见 那就是做出改变。其他数据库连接仍会看到 操作系统磁盘缓存缓冲区中的信息具有 尚未更改。因此,即使一个进程很忙 修改数据库后,其他进程可以继续读取其 拥有原始数据库内容的副本。
3.7. 将回滚日志文件刷新到大容量存储
下一步是刷新回滚日志的内容 文件到非易失性存储。 正如我们稍后将看到的, 这是确保数据库能够生存的关键步骤 意外断电。 这一步也需要很多时间,因为写入非易失性 存储通常是一个缓慢的操作。
此步骤通常比简单的冲洗更复杂 将日志回滚到磁盘。在大多数平台上,两个独立的 flush(或 fsync())操作是必需的。第一次刷新写入输出基本回滚日志内容。然后是修改回滚日记帐以显示 回滚日志。然后将标头刷新到磁盘。细节关于我们为什么要这样做,提供了标题修改和额外的刷新在本文的后面部分。
3.8. 获取独占锁
在对数据库文件本身进行更改之前,我们必须 获取数据库文件的独占锁。获取 独占锁实际上是一个两步过程。首先 SQLite 获得 “待定”锁。然后,它将挂起的锁升级为 独占锁。
挂起的锁允许其他进程已经具有共享锁继续读取数据库文件。但它阻止建立新的共享锁。理念挂起锁的背后是为了防止写入器饥饿造成的由一大群读者。可能有几十个,甚至几百个, 尝试读取数据库文件的其他进程。每个过程在开始读取之前获取共享锁,读取它的内容 needs,然后释放共享锁。但是,如果有许多不同的进程都从同一个数据库读取,它可能会发生新进程始终在之前获取其共享锁的情况 上一个进程释放其共享锁。所以有当数据库上没有共享锁时,永远不会有瞬间 文件,因此作者永远没有机会 抓住专属锁。挂起的锁旨在防止 通过允许现有共享锁继续进行,但阻止建立新的共享锁。最终 所有共享锁都将清除,待处理的锁将被清除 能够升级为独占锁。
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 检查是否 回滚日志是“热门日志”。热门期刊是 需要回放的回滚日志 将数据库还原到正常状态。仅热期刊 当早期进程处于提交过程中时存在 崩溃或断电时的交易。
如果满足以下所有条件,则回滚日志是“热”日志 是真的:
- 回滚日志存在。
- 回滚日志不是空文件。
- 主数据库文件上没有保留锁。
- 回滚日志的标题格式正确,特别是 尚未归零。
- 回滚日志不 包含超级日志文件的名称(请参阅下面的第 5.5 节),或者如果包含 包含超级期刊的名称,然后包含该超级期刊的名称 文件存在。
热期刊的存在是我们的指示 以前的进程正在尝试提交事务,但 在完成之前,由于某种原因中止了 犯。热门期刊意味着 数据库文件处于不一致状态,需要 在使用前进行修复(通过回滚)。
4.3. 获取数据库的独占锁
处理热门期刊的第一步是 获取数据库文件的独占锁。这样可以防止两个 或尝试回滚同一热日志的更多进程 同时。
4.4. 回滚不完整的更改
一旦进程获得独占锁,它就被允许了 写入数据库文件。然后,它继续读取 页面的原始内容从回滚日志中取出并写入 该内容返回到数据库文件中的来源位置。 回滚日志的标题记录了原始日志 开始中止之前数据库文件的大小 交易。SQLite 使用此信息截断 数据库文件恢复到其原始大小,如果 事务不完整导致数据库增长。在 在此步骤结束时,数据库的大小应相同且 包含与开始之前相同的信息 中止的事务。
4.5. 删除热门日志
回滚日志中的所有信息都已 播放到数据库文件中(并刷新到磁盘以防万一 我们又遇到了一次电源故障),热回滚日志 可以删除。
如第 3.11 节所述,日记 文件可能被截断为零长度,或者其标题可能被截断 被零覆盖,作为对以下系统的优化 删除文件的成本很高。无论哪种方式,日记都不是 此步骤后更热。
4.6. 继续,就好像未完成的写入从未发生过一样
最后的恢复步骤是减少独占锁定 到共享锁。一旦发生这种情况,数据库就会回到 声明如果中止的事务从未发生过,就会如此 开始。由于所有这些恢复活动都完全发生 自动且透明地,它显示给程序使用 SQLite,就好像中止的事务从未开始一样。
5. 多文件提交
SQLite允许与单个数据库连接进行通信 两个或多个数据库文件同时通过使用 ATTACH DATABASE 命令。 在单个数据库文件中修改多个数据库文件时 事务,所有文件都以原子方式更新。 换言之,要么更新所有数据库文件,要么 否则都不是。 跨多个数据库文件实现原子提交是 比对单个文件这样做更复杂。本节 描述了 SQLite 如何工作。
5.1. 为每个数据库单独回滚日志
当事务中涉及多个数据库文件时, 每个数据库都有自己的回滚日志和每个数据库 单独锁定。右图显示了一个方案 其中修改了三个不同的数据库文件 一笔交易。这一步的情况类似于 步骤 3.6 中的单文件事务方案。每个数据库文件都有 保留锁。对于每个数据库,页面的原始内容 正在更改的内容已写入回滚日志 对于该数据库,但期刊的内容尚未 已刷新到磁盘。未对数据库进行任何更改 文件本身,尽管可能正在举行更改 在用户内存中。
为简洁起见,本节中的图表从 那些以前来的。蓝色仍然表示原创内容 粉红色仍然表示新内容。但是个别页面 在回滚日志和数据库文件中未显示,并且 我们没有区分 操作系统缓存和磁盘上的信息。所有 这些因素仍然适用于多文件提交方案。他们 只是在图表中占用了大量空间,并且不会添加 任何新信息,因此此处省略。
5.2. 超级日志文件
多文件提交的下一步是创建一个 “超级日志”文件。超级日志文件的名称是 与原始数据库文件名(数据库 使用 sqlite3_open() 接口打开, 不是 ATTACHed 辅助人员之一 databases),并附加文本“-mjHHHHHHHHH”,其中 HHHHHHHH 是随机的 32 位十六进制数。这 每个新的超级期刊的随机 HHHHHHHH 后缀更改。
(注意:计算超级期刊文件名的公式 上一段中给出的对应于实现为 SQLite 版本 3.5.0。但这个公式不是SQLite的一部分 规范,并可能在将来的版本中发生变化。
与回滚日志不同,超级日志不包含 任何原始数据库页面内容。取而代之的是,超级期刊包含 每个数据库的回滚日志的完整路径名 参与交易。
超级日志构建完成后,其内容将被刷新 在执行任何进一步操作之前,先磁盘。在 Unix 上,目录 包含超级日志的也同步,以确保 超级日志文件将出现在目录中,在 电源故障。
超级期刊的目的是确保多文件 在断电期间,事务是原子的。但是,如果数据库文件 具有其他影响断电事件完整性的设置 (例如 PRAGMA synchronous=OFF 或 PRAGMA journal_mode=MEMORY) 则 作为优化,省略了超级期刊的创建。
5.3. 更新回滚日志头
下一步是记录超级日志文件的完整路径名 在每个回滚日志的标题中。空间来容纳 超级日志文件名保留在每个回滚日志的开头 创建回滚日志时。
每个回滚日志的内容都会刷新到磁盘 并且将超级日志文件名写入回滚后 日志标题。进行这两种冲洗都很重要。幸运 第二次冲洗通常很便宜,因为通常只有一次 日志文件的页面(第一页)已更改。
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 在标题中记录回滚日志中的页数 的回滚日志。此数字最初为零。所以在 尝试回滚不完整(可能已损坏)的回滚 journal,则进行回滚的进程将看到 journal 包含零页,因此不会对数据库进行任何更改。事先 对于提交,回滚日志将刷新到磁盘,以确保 所有内容都已同步到磁盘,没有“垃圾” ,只有这样,标题中的页数才会从 回滚日志中的页数为零到真实页数。回滚日志 页眉始终保存在与任何页面数据不同的扇区中,以便 它可以被覆盖和刷新,而不会有损坏数据的风险 页面(如果发生停电)。请注意,回滚日志 刷新到磁盘两次:一次用于写入页面内容,第二次用于写入页面内容 是时候在标题中写入页数了。
上一段描述了当 同步编译指示设置为“已满”。
PRAGMA 同步=FULL;
默认同步设置是 full 所以上面是通常的 发生。但是,如果同步设置降低到“正常”, SQLite 仅在页数达到 被写了。 这会带来腐败的风险,因为可能会发生 修改后的(非零)页计数首先到达磁盘表面 的数据确实如此。数据将首先写入,但 SQLite 假定底层文件系统可以对写入请求进行重新排序,并且 页数可以先烧成氧化物,即使它 写入请求最后发生。因此,作为第二道防线,SQLite 还对回滚中的每一页数据使用 32 位校验和 杂志。在回滚期间,将针对每个页面计算此校验和 在第 4.4 节中描述回滚日志时。如果校验和不正确 被看到,回滚被放弃。请注意,校验和 不能保证页面数据正确,因为有一个小 但校验和可能是正确的概率有限,即使数据是正确的 腐败。但是校验和至少使这种错误不太可能发生。
请注意,回滚日志中的校验和不是必需的 如果同步设置为 FULL。我们只依赖于校验和 当同步降低到 NORMAL 时。尽管如此,校验和 永远不会受到伤害,因此无论如何它们都包含在回滚日志中 的同步设置。
6.3. 提交前的缓存溢出
第 3.0 节中显示的提交过程假定所有数据库更改都适合内存,直到需要 犯。这是常见的情况。但有时更大的变化会 在事务提交之前溢出用户空间缓存。在那些 情况下,缓存必须在事务之前溢出到数据库 已完成。
在缓存溢出开始时,数据库的状态 连接如步骤 3.6 所示。 原始页面内容已保存在回滚日志中,并且 页面的修改存在于用户内存中。要溢出缓存, SQLite 执行步骤 3.7 到 3.9。换言之,回滚日志刷新到磁盘,获取独占锁,更改 写入数据库。但其余步骤被推迟 直到事务真正提交。新的日记帐标题是 附加到回滚日志的末尾(在其自己的扇区中) 并保留独占数据库锁,但以其他方式进行处理 返回到步骤 3.6。当交易 提交,或者如果发生另一个缓存溢出,步骤 3.7 和 3.9 是 重复。(步骤 3.8 在第二个省略 以及由于已持有独占数据库锁而进行的后续传递 由于第一次通过。
缓存溢出导致数据库文件上的锁定 从保留升级到独占。这会降低并发性。 缓存溢出还会导致额外的磁盘刷新或 fsync 操作 发生并且这些操作很慢,因此缓存溢出可能会 严重降低性能。 由于这些原因,应尽可能避免缓存溢出。
7. 优化
分析表明,对于大多数系统和大多数情况 SQLite 大部分时间都花在磁盘 I/O 上。因此, 我们可以采取任何措施来减少磁盘 I/O 的数量,都可能有一个 对SQLite的性能有很大的积极影响。本节 描述了 SQLite 用来尝试减少 磁盘 I/O 的数量降至最低,同时仍保留原子提交。
7.1. 事务之间保留的缓存
提交过程的步骤 3.12 显示 一旦共享锁被释放,所有用户空间缓存 必须丢弃数据库内容的图像。这样做是因为 如果没有共享锁,其他进程可以自由修改数据库 文件内容,因此该内容的任何用户空间图像都可能成为 过时。因此,每笔新交易都将从重读开始 之前已读取的数据。这并不像听起来那么糟糕 起初,由于正在读取的数据仍然可能在操作中 系统文件缓存。因此,“读取”实际上只是数据的副本 从内核空间到用户空间。但即便如此,这仍然需要时间。
从 SQLite 版本 3.3.14 开始,添加了一种机制 以尝试减少不必要的数据重读。在较新的版本中 ,用户空间寻呼机缓存中的数据在以下情况下保留 数据库文件上的锁定被释放。后来,在 共享锁是在下一次交易开始时获得的, SQLite检查是否有任何其他进程修改了数据库 文件。如果数据库自锁定以来以任何方式更改 上次发布时,用户空间缓存将在此时擦除。 但通常数据库文件保持不变,用户空间缓存 可以保留,可以避免一些不必要的读取操作。
为了确定数据库文件是否已更改, SQLite 在数据库标头中使用计数器(以字节 24 到 27 为单位) 在每次更改操作期间递增。SQLite 保存副本 在释放其数据库锁之前,此计数器。然后之后 获取下一个数据库锁,它比较保存的计数器值 针对当前计数器值,如果值 不同,如果它们相同,则重用缓存。
7.2. 独占访问模式
SQLite 版本 3.3.14 增加了“独占访问模式”的概念。 在独占访问模式下,SQLite 保留独占 数据库在每个事务结束时锁定。这可以防止 访问数据库的其他进程,但在许多部署中 只有一个进程使用数据库,所以这不是一个 严重的问题。独占访问模式的优点是 可以通过三种方式减少磁盘 I/O:
-
没有必要在 第一个事务之后的事务的数据库标头。这 通常会将第一页的写入保存到回滚中 日志和主数据库文件。
-
没有其他进程可以更改数据库,因此永远不会更改 需要检查更改计数器并清除用户空间缓存 在交易开始时。
-
每个事务都可以通过覆盖回滚来提交 日志标题为零,而不是删除日志文件。 这样就避免了修改日志文件的目录条目 并且它避免了必须取消分配与 杂志。此外,下一个事务将覆盖现有的 日志文件内容,而不是附加新内容,并且在大多数系统上 覆盖比追加快得多。
第三个优化,将日志文件头归零,而不是删除回滚日志文件, 不依赖于始终持有独占锁。 此优化可以独立于独占锁定模式进行设置 使用下面第 7.6 节中描述的journal_mode编译指示。
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 节的操作。 而不是为了提交事务而删除日志文件, 文件的长度被截断为零字节,或者其 标头被零覆盖。将文件截断为零 length 省去了对包含 文件,因为该文件不会从目录中删除。 覆盖标头可以节省额外的费用,因为没有 更新文件的长度(在许多系统的“inode”中) 并且不必处理新释放的磁盘扇区。此外 在下一个事务中,将通过覆盖创建日记帐 现有内容,而不是将新内容附加到末尾 ,覆盖通常比追加快得多。
SQLite可以配置为通过覆盖来提交事务 带有零的日记标题,而不是删除日志文件 通过使用 PRAGMA journal_mode设置“PERSIST”日记模式。 例如:
PRAGMA journal_mode=PERSIST;
使用持久日志模式可提供显著的性能 许多系统的改进。当然,缺点是 日志文件保留在磁盘上,占用磁盘空间和杂乱无章 目录,在事务提交后很久。唯一安全的方法 删除持久日志文件就是提交事务 将日记模式设置为 DELETE:
PRAGMA journal_mode=DELETE; BEGIN EXCLUSIVE; COMMIT;
谨防通过任何其他方式删除持久性日志文件 由于日志文件可能很热,在这种情况下,删除它将 损坏相应的数据库文件。
从 SQLite 版本 3.6.4 (2008-10-15) 开始, TRUNCATE 日志模式是 还支持:
PRAGMA journal_mode=TRUNCATE;
在截断日志模式下,通过截断来提交事务 日志文件长度为零,而不是删除日志文件 (如在 DELETE 模式下)或通过将标头归零(如在 PERSIST 模式下)。 TRUNCATE 模式具有 PERSIST 模式的优点,即目录 包含日志文件和数据库的数据库不需要更新。 因此,截断文件通常比删除文件更快。TRUNCATE 有 它后面没有 系统调用(例如:fsync())将更改同步到磁盘。它可能 如果是这样,会更安全。 但是在许多现代文件系统上,截断是原子和 同步操作,因此我们认为 TRUNCATE 通常是安全的 面对停电。如果您不确定是否或 不是 TRUNCATE 将在您的文件系统上同步和原子化,它是 对您来说,重要的是您的数据库在断电或运行时能够幸存下来 在截断操作期间发生的系统崩溃,那么您可能会 请考虑使用其他日记模式。
在具有同步文件系统的嵌入式系统上,TRUNCATE 结果 行为比 PERSIST 慢。提交操作的速度相同。 但是在 TRUNCATE 之后,后续事务会变慢,因为它是 覆盖现有内容比追加到文件末尾更快。 新的日志文件条目将始终附加在 TRUNCATE 之后,但 通常会用 PERSIST 覆盖。
8. 测试原子提交行为
SQLite的开发人员相信它是健壮的 面对电源故障和系统崩溃,因为 自动测试程序对 SQLite从模拟功率损耗中恢复的能力。 我们称之为“碰撞测试”。
SQLite 中的崩溃测试使用修改后的 VFS,可以模拟 通电期间发生的文件系统损坏类型 丢失或操作系统崩溃。碰撞测试 VFS 可以模拟 扇区写入不完整,页面充满垃圾数据,因为 写入未完成,写入顺序不正常,全部发生 在测试场景的不同时间点。执行碰撞测试 事务一遍又一遍,改变模拟的时间 发生功率损失和造成的损坏的性质。 然后,每个测试在模拟崩溃后重新打开数据库,并 验证事务是否完全发生 或者根本没有,并且数据库完全处于 一致状态。
SQLite中的崩溃测试发现了许多非常 恢复机制中的细微错误(现已修复)。一些 这些错误非常晦涩难懂,不太可能被发现 仅使用代码检查和分析技术。从这里 经验,SQLite的开发人员对任何其他人都充满信心 不使用类似碰撞测试系统的数据库系统 可能包含未检测到的 bug,这些 bug 将导致数据库 系统崩溃或电源故障后的损坏。
9. 可能出错的事情
SQLite中的原子提交机制已被证明是健壮的, 但它可以通过一个足够有创意的人来规避 对手或足够损坏的操作系统实现。 本节介绍 SQLite 数据库的几种方式 可能因电源故障或系统崩溃而损坏。 (另请参阅:如何损坏数据库文件。
9.1. Broken Locking 实现
SQLite 使用文件系统锁来确保只有一个 进程和数据库连接正在尝试修改数据库 一次。实现文件系统锁定机制 在 VFS 层中,并且每个操作系统都不同。 SQLite取决于此实现是否正确。如果有什么 出错,两个或多个进程能够写入相同的内容 数据库文件,否则会造成严重损坏。
我们收到了关于这两项措施实施情况的报告 Windows 网络文件系统和 NFS,其中锁定 微妙地破碎了。我们无法验证这些报告,但作为 在网络文件系统上很难正确锁定 我们没有理由怀疑他们。建议您 首先避免在网络文件系统上使用 SQLite, 因为性能会很慢。但是,如果您必须使用 网络文件系统来存储 SQLite 数据库文件,请考虑 使用辅助锁定机制防止同时发生 即使本机文件系统也写入同一数据库 锁定机构故障。
Apple 上预装的 SQLite 版本 Mac OS X 计算机包含的 SQLite 版本已 扩展为使用适用于以下问题的替代锁定策略 Apple 支持的所有网络文件系统。这些扩展 只要所有进程都在访问,Apple 就可以很好地工作 以同样的方式处理数据库文件。不幸的是,锁定 机制不会相互排斥,因此如果一个过程是 使用(例如)AFP 锁定和其他 进程(可能在另一台机器上)使用点文件锁, 这两个进程可能会发生冲突,因为 AFP 锁不排除 点文件锁定,反之亦然。
9.2. 磁盘刷新不完整
SQLite 在 Unix 和 FlushFileBuffers() 上使用 fsync() 系统调用 在 W32 上调用系统以将文件系统缓冲区同步到磁盘上 氧化物如步骤3.7和步骤3.10所示。不幸的是,我们收到了 报告说,这些接口都不像许多接口所宣传的那样工作 系统。我们听说 FlushFileBuffers() 可以完全禁用 在某些 Windows 版本上使用注册表设置。一些 历史的 Linux 版本包含 fsync() 的版本,这些版本对 我们被告知,一些文件系统。即使在以下系统上 据说 FlushFileBuffers() 和 fsync() 经常工作 IDE磁盘控制撒谎并说数据已到达氧化物 而它仍然仅保存在易失性控制缓存中。
在 Mac 上,您可以设置以下编译指示:
PRAGMA fullfsync=ON;
在 Mac 上设置 fullfsync 将保证数据确实有效 在冲洗时被推到圆盘盘上。但是实施 的 fullfsync 涉及重置磁盘控制器。所以不仅 它是否非常慢,它还减慢了其他不相关的磁盘 I/O。 因此,不建议使用它。
9.3. 部分文件删除
SQLite 假设文件删除是 用户进程的观点。如果电源在中间发生故障 删除文件,然后在电源恢复后 SQLite 希望看到 要么是整个文件,其所有原始数据都完好无损,要么 预计根本找不到文件。事务可能不是原子的 在无法以这种方式工作的系统上。
9.4. 写入文件的垃圾
SQLite数据库文件是普通的磁盘文件,可以 由普通用户进程打开和写入。流氓进程 可以打开SQLite数据库并用损坏的数据填充它。 损坏的数据也可能被引入SQLite数据库 操作系统或磁盘控制器中的错误;特别是 由电源故障触发的 Bug。SQLite无能为力 做来防御这类问题。
9.5. 删除或重命名热门日志
如果确实发生崩溃或断电,并且热日志保持打开状态 磁盘上,必须要有原始的数据库文件和热的 日志以其原始名称保留在磁盘上,直到数据库 文件由另一个 SQLite 进程打开并回滚。 在步骤 4.2 的恢复期间,SQLite 定位 热日志 通过查找与 正在打开的数据库,其名称派生自 文件正在打开。如果原始数据库文件或 热门日记已被移动或重命名,则热门日记将 不被看到,数据库也不会回滚。
我们怀疑SQLite恢复的常见故障模式发生了 像这样:发生电源故障。电力恢复后,一个善意的 用户或系统管理员开始在磁盘上四处查找 损伤。他们看到名为“important.data”的数据库文件。此文件 也许是他们熟悉的。但崩盘之后,还有一个 名为“important.data-journal”的热门期刊。然后,用户删除 热门期刊,认为他们正在帮助清理系统。 我们知道除了用户教育之外,没有其他方法可以防止这种情况。
如果数据库文件有多个(硬链接或符号链接), 日记帐将使用链接的名称创建,通过该链接 文件已打开。如果发生崩溃并再次打开数据库 使用其他链接,将找不到热日志,也不会找到 将发生回滚。
有时电源故障会导致文件系统损坏 这样,最近更改的文件名就会被遗忘,而文件是 移至“/lost+found”目录。当这种情况发生时,热 找不到日记帐,也不会进行恢复。 SQLite试图阻止这种情况 通过打开并同步包含回滚日志的目录 同时,它同步日志文件本身。但是, 将文件移动到 /lost+found 可能是由不相关的进程引起的 在与主数据库文件相同的目录中创建不相关的文件。 而且由于这是在SQLite的控制之下,所以什么都没有 SQLite可以做些什么来防止它。如果您在以下系统上运行 容易受到这种文件系统命名空间损坏的影响(大多数 我们相信,现代日志文件系统是免疫的)那么你可能会 想考虑将每个 SQLite 数据库文件放在自己的私有文件中 子目录。
10. 未来的方向和结论
时不时有人会发现一种新的故障模式 SQLite中的原子提交机制,开发人员必须 放入补丁。这种情况越来越少,而且 故障模式变得越来越模糊。但它会 仍然愚蠢地假设 SQLite是完全没有错误的。开发人员致力于修复 这些错误可能会尽快被发现。
开发人员也在寻找新的方法 优化提交机制。当前的 VFS 实现 对于 Unix(Linux 和 Mac OS X)和 Windows 做出悲观的假设 这些系统的行为。咨询专家后 关于这些系统是如何工作的,我们也许可以放松一些 对这些系统的假设,并允许它们运行得更快。在 特别是,我们怀疑大多数现代文件系统都表现出 安全附加属性,其中许多可能支持原子 部门写道。但在确定这一点之前,SQLite 将 采取保守的方法,做最坏的打算。