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.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然后检查是否 回滚日志是一个“热日志”。 热门期刊是 需要回放的回滚日志,以便 将数据库恢复到正常状态。 仅限热门日志 当早期进程处于提交过程中时存在 崩溃或断电时的事务。
如果满足以下所有条件,回滚日志就是“热”日志 均为真:
回滚日志存在。 回滚日志不是空文件。 主数据库文件上没有保留锁。 回滚日志的标头格式良好,尤其是 尚未清零。 回滚日志没有 包含超级日志文件的名称(请参见 第5.5节 或如果是 包含超级日志的名称,然后是该超级日志 文件存在。
热门期刊的出现是我们的迹象 前一个进程试图提交事务,但 它在完成 提交。 热门期刊意味着 数据库文件处于不一致状态,需要 在使用之前修复(通过回滚)。
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=内存 )然后 作为优化,省略了超级日志的创建。
下一步是记录超级日志文件的完整路径名 在每个回滚日志的标头中。 容纳的空间 每个回滚日志的开头都保留了超级日志文件名 当创建回滚日志时。
之前,每个回滚日志的内容都会刷新到磁盘 并在将超级日志文件名写入回滚之后 日记帐标题。 这两种刷新都很重要。 幸运的是, 第二次冲洗通常很便宜,因为通常只有一次 日志文件的第页(第一页)已更改。
此步骤类似于 步骤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.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检查是否有其他进程修改了数据库 文件。 如果自锁定后数据库已以任何方式更改 上一次发布时,用户空间缓存将被擦除。 但通常数据库文件保持不变,用户空间缓存 可以保留,并且可以避免一些不必要的读取操作。
为了确定数据库文件是否已更改, SQLite在数据库头中使用计数器(以字节24到27为单位) 其在每次改变操作期间递增。 SQLite保存副本 在释放其数据库锁之前。 之后 获取下一个数据库锁-比较保存的计数器值 根据当前计数器值,如果值 不同,或者如果缓存相同,则重用缓存。
7.2. 独占访问模式
SQLite版本3.3.14添加了“独占访问模式”的概念。 在独占访问模式下,SQLite保留独占 在每个事务结束时锁定数据库。 这样可以防止 访问数据库的其他进程,但在许多部署中 只有一个进程正在使用数据库,因此这不是 严重问题。 独占访问模式的优点是 可以通过三种方式减少磁盘I/O:
无需在 第一个事务之后的事务的数据库头。 这个 通常会将第一页的写入保存到两个回滚 日志和主数据库文件。
没有其他进程可以更改数据库,因此永远不会 需要检查更改计数器并清除用户空间缓存 在事务开始时。
可以通过覆盖回滚来提交每个事务 带有零的日志头,而不是删除日志文件。 这样可以避免修改日志文件的目录条目 并且它避免了必须取消分配与 日记账。 此外,下一个事务将覆盖现有的 日志文件内容,而不是在大多数系统上附加新内容 覆盖比追加快得多。
第三个优化是将日志文件头归零,而不是 删除回滚日志文件, 不依赖于始终持有独占锁。 此优化可以独立于独占锁定模式进行设置 使用 日志模式杂注 如中所述 第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.2 SQLite位于 通过在与 数据库的名称派生自 正在打开的文件。 如果原始数据库文件或 已移动或重命名热日志,则该热日志将 不可见,数据库也不会回滚。
我们怀疑发生了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 联合技术公司