SQLite 版本 3.0.0 引入了新的锁定和日志功能 旨在提高 SQLite 版本 2 的并发性的机制 并减少作家的饥饿 问题。新机制还允许交易的原子提交 涉及多个数据库文件。 本文档介绍新的锁定机制。 目标受众是想要理解和/或修改的程序员页面代码和审阅者致力于验证设计 的 SQLite 版本 3。
锁定和并发控制由页面模块处理。 页面模块负责使 SQLite 成为“ACID”(原子、 一致、隔离和持久)。页面模块确保更改 一下子发生,要么所有的变化都发生,要么没有一个发生, 两个或多个进程不尝试访问数据库 同时以不兼容的方式,并且一旦发生了变化 书面的它们会一直存在,直到被明确删除。页面还提供磁盘文件某些内容的内存缓存。
页面不关心 以及 B 树、文本编码、索引等的详细信息。 从页面的角度来看,数据库由大小均匀的块的单个文件。每个块称为 “page”,大小通常为 1024 字节。页面已编号 从 1 开始。因此,数据库的前 1024 个字节被称为 “第 1 页”和第二个 1024 字节称为“第 2 页”,依此类推。都 其他编码细节由库的更高层处理。 页面使用以下几种之一与操作系统进行通信模块 (例如: os_unix.c、 os_win.c) 这为操作系统服务提供了统一的抽象。
页面模块有效地控制对单独线程的访问,或者单独的进程,或两者兼而有之。在本文档中,每当 写有“进程”一词,您可以用“线程”一词代替,而不使用 改变陈述的真实性。
从单个进程的角度来看,数据库文件 可以处于以下五种锁定状态之一:
解 锁 | 数据库上不保留任何锁。数据库可能既不能读取,也不能 写。任何内部缓存的数据都被视为可疑数据,并受 在使用之前对数据库文件进行验证。其他 进程可以读取或写入数据库作为自己的锁定状态 许可证。这是默认状态。 |
共享 | 可以读取数据库,但不能写入数据库。任意数量的 进程可以同时持有 SHARED 锁,因此可以有 许多同时阅读器。但不允许使用其他线程或进程 在一个或多个 SHARED 锁处于活动状态时写入数据库文件。 |
保留 | RESERVED 锁表示进程正计划写入 数据库文件,但它目前只是 从文件中读取。一个只能有一个 RESERVED 锁处于活动状态 时间,但多个 SHARED 锁可以与单个 RESERVED 锁共存。 RESERVED 与 PENDING 的不同之处在于可以获取新的 SHARED 锁 而有一个 RESERVED 锁。 |
待定 | PENDING 锁表示持有该锁的进程想要写入 尽快到数据库,只是在等待所有当前 要清除的 SHARED 锁,以便它可以获得 EXCLUSIVE 锁。没有新的 如果出现以下情况,则允许对数据库使用 SHARED 锁 PENDING 锁处于活动状态,但允许现有 SHARED 锁 继续。 |
独家 | 需要 EXCLUSIVE 锁才能写入数据库文件。 文件上只允许有一个 EXCLUSIVE 锁,不允许使用其他锁 任何种类都允许与 EXCLUSIVE 锁共存。为了 最大化并发性,SQLite 致力于最大限度地减少 持有独家锁。 |
操作系统接口层理解并跟踪所有五个 如上所述的锁定状态。 页面模块仅跟踪五种锁定状态中的四种。 PENDING 锁始终只是临时锁 通往 EXCLUSIVE 锁的垫脚石,因此页面模块 不跟踪 PENDING 锁。
当进程想要更改数据库文件时(但事实并非如此) 在 WAL 模式下),它 首先记录原始未更改的数据库内容 在回滚日志中。回滚日志是普通的 始终位于的磁盘文件 在与数据库文件相同的目录或文件夹中,并具有 与数据库文件同名,但添加了 -journal 后缀。回滚日志还记录初始 数据库的大小,以便在数据库文件增长时可以截断它 回滚时恢复到其原始大小。
如果SQLite同时使用多个数据库 (使用 ATTACH 命令)则每个数据库都有自己的回滚日志。 但还有一个单独的聚合日志称为超级期刊。 超级日志不包含用于回滚的页面数据变化。取而代之的是,超级期刊包含每个ATTACHed数据库的单独数据库回滚日志。 每个单独的数据库回滚日志还包含名称 的超级期刊。 如果没有ATTACHed数据库(或者没有ATTACHed 数据库) 正在参与当前交易)没有超级期刊是已创建,并且正常回滚日志包含空字符串在通常保留用于记录名称的地方 超级期刊。
如果回滚日志需要回滚,则称其为热回滚 为了恢复其数据库的完整性。 当进程位于数据库中间时,将创建热日志 更新和程序或操作系统崩溃或电源故障可防止 从完成开始的更新。 热日志是一种例外情况。 存在热日志以从崩溃和电源故障中恢复。 如果一切正常 (也就是说,如果没有崩溃或电源故障) 你永远不会得到一本热门的日记。
如果不涉及超级期刊,则 如果日记存在并且具有非零标题,则该日记为热 及其对应的数据库文件 没有 RESERVED 锁。 如果在文件日记中命名了超级日志,则文件日志 如果它的超级日志存在并且没有 RESERVED 锁定相应的数据库文件。 了解期刊何时热门很重要,因此 前面的规则将在项目符号中重复:
在从数据库文件读取之前,SQLite始终会检查是否 数据库文件具有热日志。如果文件确实有热日志,则 在读取文件之前回滚日志。通过这种方式,我们确保 数据库文件在读取之前处于一致状态。
当进程想要从数据库文件中读取时,它遵循 以下步骤顺序:
上述算法成功完成后,可以安全地 从数据库文件中读取。完成所有读取后, SHARED 锁被丢弃。
过时的超级日志是不再存在的超级日志用于任何事情。没有要求过时的超级日志被删除。这样做的唯一原因是释放磁盘空间。
如果没有单个文件日志指向,则超级日志已过时到它。为了弄清楚超级日志是否过时,我们首先阅读 super-journal,以获取其所有文件日志的名称。然后 我们检查每个文件日志。如果任何文件日志命名为在超级日志中存在并指向超级日志,那么超级日志并没有过时。如果所有文件日志都丢失 或参考其他超级日志或根本没有超级日志,则 我们正在测试的超级日志已经过时,可以安全地删除。
要写入数据库,进程必须首先获取 SHARED 锁 如上所述(如果有,可能会回滚不完整的更改 是一本热门期刊)。 获取 SHARED 锁后,必须获取 RESERVED 锁。 RESERVED 锁表示进程打算写入 数据库在将来的某个时候。一次只能处理一个进程 可以持有 RESERVED 锁。但其他进程可以继续读取 数据库,而保留 RESERVED 锁。
如果要写入的进程无法获取 RESERVED lock,这必须意味着另一个进程已经具有 RESERVED 锁。 在这种情况下,写入尝试将失败并返回SQLITE_BUSY。
获取RESERVED锁后,想要写入的进程创建回滚日志。日志的标题已初始化替换为数据库文件的原始大小。日记帐标题中的空格也保留给超级期刊名称,尽管超级期刊name最初为空。
在对数据库的任何页面进行更改之前,该过程会写入将该页面的原始内容添加到回滚日志中。变化 “到”页首先保存在内存中,不会写入磁盘。 原始数据库文件保持不变,这意味着其他 进程可以继续读取数据库。
最终,写入过程将需要更新数据库文件,要么是因为它的内存缓存已满,要么是因为它是准备提交其更改。在此之前,编写器必须确保没有其他进程正在读取数据库,并且回滚日志数据安全地位于磁盘表面,以便可用于回滚 电源故障时更改不完整。 步骤如下:
如果写入数据库文件的原因是因为内存缓存已满,则编写器不会立即提交。相反作者可能会继续对其他页面进行更改。以前后续更改将写入数据库文件,即回滚日志必须再次刷新到磁盘。另请注意,EXCLUSIVE写入器最初为了写入数据库而获取的锁必须保留,直到提交所有更改。这意味着没有其他进程能够从 内存缓存首次溢出到磁盘的时间,直到事务发生 提交。
当编写器准备好提交其更改时,它会执行以下命令 步骤:
一旦从数据库文件中释放 PENDING 锁,其他 进程可以再次开始读取数据库。在当前的实现中, RESERVED 锁也会被释放,但这不是必需的 正确操作。
如果一个事务涉及多个数据库,那么一个更复杂的 使用提交序列,如下所示:
在 SQLite 版本 2 中,如果许多进程正在从数据库中读取, 可能从来没有一个时候没有活跃的读者。如果 数据库,则任何进程都无法对数据库进行更改 因为不可能获得写锁。这种情况 被称为作家饥饿。
SQLite 版本 3 旨在通过使用 PENDING 锁。PENDING 锁允许现有读卡器继续 但会阻止新读取器连接到数据库。因此,当 进程想要写一个繁忙的数据库,它可以设置一个 PENDING 锁 将阻止新读者进入。假设现有读者这样做 最终完成,所有共享锁最终将清除,并且 作者将有机会进行更改。
寻呼机模块非常强大,但它可以被颠覆。 本节试图识别和解释风险。 (另请参阅本文的“可能出错的事情”部分 在 Atomic Commit 上。
显然,引入错误数据的硬件或操作系统故障 进入数据库文件或日志的中间会引起问题。 同样 如果恶意进程打开数据库文件或日志并写入格式不正确的文件 数据进入其中,那么数据库就会损坏。 对于这类问题,我们无能为力 所以他们没有得到进一步的关注。
SQLite 使用 POSIX 咨询锁在 Unix 上实现锁定。上 Windows 它使用 LockFile()、LockFileEx() 和 UnlockFile() 系统 调用。SQLite 假定这些系统调用的所有工作都按通告的方式进行。如果 事实并非如此,则可能导致数据库损坏。一个人应该 请注意,已知 POSIX 咨询锁定存在错误,甚至未实现 在许多 NFS 实现(包括最新版本的 Mac OS X)上 并且有关于锁定问题的报告 适用于 Windows 下的网络文件系统。你最好的防御就是不要 对网络文件系统上的文件使用 SQLite。
SQLite 使用 fsync() 系统调用将数据刷新到 Unix 下的磁盘,并且 它使用 FlushFileBuffers() 在 Windows 下执行相同的操作。再来一次 SQLite 假定这些操作系统服务按通告的方式运行。 但据报道,fsync() 和 FlushFileBuffers() 并不总是 正常工作,尤其是对于某些网络文件系统或廉价的 IDE 磁盘。 显然,一些IDE磁盘制造商的控制器芯片报告 该数据已到达磁盘表面,而实际上数据仍在 在磁盘驱动器电子设备中的易失性缓存内存中。还有 报告 Windows 有时会选择忽略 FlushFileBuffers() 原因不明。作者无法核实任何这些报告。 但如果它们是真的,那就意味着数据库损坏是可能的 意外断电后。这些是硬件和/或操作 SQLite无法防御的系统错误。
如果挂载的 Linux ext3 文件系统没有 “barrier=1” 选项 在 /etc/fstab 中,磁盘驱动器已启用写入缓存 那么文件系统损坏可能会在断电或操作系统崩溃后发生。 是否发生损坏取决于磁盘控制的详细信息 硬件;使用廉价的消费级磁盘时,损坏的可能性更大 对于具有高级的企业级存储设备来说,问题更少 非易失性写入缓存等功能。 各种 ext3 专家证实了这种行为。 我们被告知大多数 Linux 发行版不使用 barrier=1 并且使用 不禁用写缓存,所以大多数 Linux 发行版容易受到此问题的影响。请注意,这是一个 操作系统和硬件问题,并且没有SQLite的内容 可以做来解决它。 其他数据库引擎也遇到了同样的问题。
如果发生崩溃或电源故障并导致日志过热,但 日志被删除,下一进程打开数据库不会 知道它包含需要回滚的更改。回滚 不会发生,并且数据库将处于不一致状态。 回滚日志可能因多种原因而被删除:
上面的最后一个(第四个)项目符号值得补充评论。当 SQLite 创建时 Unix 上的日志文件,它会打开包含该文件的目录,然后 在目录上调用 fsync(),以推送目录信息 到磁盘。但是假设其他一些进程正在添加或删除不相关的过程 文件添加到包含数据库和日志的目录中 停电的时刻。这个其他人的所谓不相关的行为 进程可能会导致日志文件从目录中删除,并且 移至“失物招领”。这种情况不太可能发生,但可能会发生。 最好的防御措施是使用日志文件系统或保留 数据库和日志单独放在一个目录中。
对于涉及多个数据库和超级日志的提交,如果 不同的数据库位于不同的磁盘卷上,并且发生电源故障 在提交期间,当计算机恢复时,磁盘可能会 以不同的名称重新装裱。或者某些磁盘可能未装载 完全。发生这种情况时,各个文件日志和 超级期刊可能无法找到对方。最坏的结果来自 这种情况是提交不再是原子的。 某些数据库可能会回滚,而其他数据库可能不会。 所有数据库将继续自洽。 若要防止此问题,请保留所有数据库 在同一磁盘卷上和/或使用完全相同的名称重新装载磁盘 停电后。
SQLite 版本 3 中对锁定和并发控制的更改也 在 SQL 中引入事务工作方式的一些细微变化 语言水平。 默认情况下,SQLite 版本 3 在自动提交模式下运行。 在自动提交模式下, 对数据库的所有更改都会在关联的所有操作后立即提交 当前数据库连接完成。
SQL 命令“BEGIN TRANSACTION”(TRANSACTION 关键字 是可选的)用于将 SQLite 从自动提交模式中取出。 请注意,BEGIN 命令不会获取数据库上的任何锁。 在 BEGIN 命令之后,当第一个 执行 SELECT 语句。在以下情况下将获得 RESERVED 锁 执行第一个 INSERT、UPDATE 或 DELETE 语句。无独家 锁定,直到内存缓存填满,并且必须 溢出到磁盘或直到事务提交。这样, 系统延迟阻止对文件的读取访问,直到 最后可能的时刻。
SQL 命令“COMMIT”实际上不会提交对 磁盘。它只是重新打开自动提交。然后,在 命令,常规自动提交逻辑接管并导致 实际提交到磁盘。 SQL 命令“ROLLBACK”也通过重新打开自动提交来运行, 但它也设置了一个标志,告诉自动提交逻辑回滚 而不是提交。
如果 SQL COMMIT 命令打开自动提交,并且自动提交逻辑 然后尝试提交更改,但失败,因为其他进程正在保留 一个 SHARED 锁,然后自动关闭自动提交。这 允许用户在 SHARED 锁之后稍后重试 COMMIT 本来有机会通关的。
如果对同一个 SQLite 数据库执行多个命令 连接时,自动提交被推迟到非常 最后一个命令完成。例如,如果 SELECT 语句正在 执行后,命令的执行将暂停,因为 返回结果。在此暂停期间,其他 INSERT、UPDATE 或 DELETE 可以对数据库中的其他表执行命令。但没有 这些更改将提交,直到原始 SELECT 语句完成。