Small. Fast. Reliable.
Choose any three.

该文档最初创建于2004年初,当时SQLite版本2仍在广泛使用,其编写目的是向已经熟悉SQLite版本2的读者介绍SQLite版本3的新概念。可能从未见过SQLite版本2,并且仅熟悉SQLite版本3。但是,本文档继续作为权威性参考,说明如何在SQLite版本3中锁定数据库文件。

该文档仅描述了针对较旧的回滚模式事务处理机制的锁定。 单独描述了用于较新的预写日志WAL模式的锁定。

1.0 SQLite版本3中的文件锁定和并发

SQLite版本3.0.0引入了一种新的锁定和日志记录机制,旨在改善SQLite版本2上的并发性并减少编写者的饥饿问题。新机制还允许涉及多个数据库文件的事务的原子提交。本文档介绍了新的锁定机制。目标读者是希望了解和/或修改传呼程序代码的程序员,以及致力于验证SQLite版本3设计的审阅者。

2.0概述

锁定和并发控制由 寻呼机模块处理 。传呼机模块负责使SQLite成为“ ACID”(原子,一致,隔离和持久)。分页器模块确保所有更改都立即发生,或者所有更改都发生,或者所有更改都没有发生,两个或多个进程不会尝试以不兼容的方式同时访问数据库,并且一旦写入更改就可以坚持到明确删除为止。分页器还提供了磁盘文件某些内容的内存缓存。

寻呼机与B树,文本编码,索引等细节无关。从寻呼机的角度来看,数据库由一个大小一致的块的单个文件组成。每个块称为“页面”,通常大小为1024字节。页面从1开始编号。因此,数据库的前1024个字节称为“第1页”,后1024个字节称为“第2页”,依此类推。所有其他编码细节由库的更高层处理。传呼器使用几个模块之一与操作系统进行通信(示例: os_unix.c os_win.c),这些模块 为操作系统服务提供统一的抽象。

寻呼机模块有效地控制对单独的线程,或单独的进程或两者的访问。在本文档中,无论何时编写“过程”一词,都可以替换“线程”一词,而无需改变陈述的真实性。

3.0锁定

从单个进程的角度来看,数据库文件可以处于以下五个锁定状态之一:

已解锁 数据库上没有锁。不能读取或写入数据库。任何内部缓存的数据都被认为是可疑的,并且在使用之前必须先针对数据库文件进行验证。其他进程可以在其自己的锁定状态允许的情况下读取或写入数据库。这是默认状态。
共享 该数据库可以读取但不能写入。任意数量的进程可以同时持有SHARED锁,因此可以有许多同时读取器。但是,当一个或多个SHARED锁处于活动状态时,不允许其他线程或进程向数据库文件写入数据。
预订的 RESERVED锁表示该进程正在计划将来在某个时候写入数据库文件,但当前仅从该文件中读取该文件。尽管多个SHARED锁可以与一个RESERVED锁共存,但一次只能激活一个RESERVED锁。RESERVED与PENDING的不同之处在于,当有RESERVED锁时,可以获取新的SHARED锁。
待办的 PENDING锁意味着持有该锁的进程希望尽快写入数据库,并且正在等待所有当前SHARED锁清除,以便可以获取EXCLUSIVE锁。如果PENDING锁处于活动状态,则不允许对数据库使用任何新的SHARED锁,尽管允许现有的SHARED锁继续。
独家的 需要EXCLUSIVE锁才能写入数据库文件。文件上仅允许一个EXCLUSIVE锁,并且不允许任何其他类型的锁与EXCLUSIVE锁共存。为了最大化并发性,SQLite致力于最小化持有EXCLUSIVE锁的时间。

操作系统界面层了解并跟踪上述所有五个锁定状态。寻呼机模块仅跟踪五个锁定状态中的四个。PENDING锁始终只是EXCLUSIVE锁路径上的临时踏脚石,因此寻呼机模块不会跟踪PENDING锁。

4.0回滚日志

当进程想要更改数据库文件(并且它不在WAL模式下)时,它首先将原始未更改的数据库内容记录在回滚日志中。回滚日志是普通磁盘文件,始终与数据库文件位于同一目录或文件夹中,并具有与数据库文件相同的名称,并带有-journal 后缀。回滚日志还记录了数据库的初始大小,因此,如果数据库文件增大,则可以在回滚时将其截断为原始大小。

如果SQLite同时使用多个数据库(使用ATTACH命令),则每个数据库都有其自己的回滚日志。但是,还有一个单独的综合期刊,称为“超级期刊”。超级期刊不包含用于回滚更改的页面数据。相反,超级期刊包含每个ATTACHed数据库的各个数据库回滚日志的名称。每个单独的数据库回滚日志还包含超级期刊的名称。如果没有ATTACHed数据库(或者没有ATTACHed数据库参与当前事务),则不会创建超级期刊,并且常规回滚日志在通常保留用于记录超级期刊名称的位置包含一个空字符串。 。

如果需要回滚以恢复其数据库的完整性,则认为回滚日志很热。当进程在数据库更新过程中并且程序或操作系统崩溃或断电导致更新无法完成时,将创建热日志。热门期刊是一个例外情况。存在热日志以从崩溃和电源故障中恢复。如果一切工作正常(也就是说,没有崩溃或电源故障),您将永远不会获得最新的日志。

如果不涉及超级期刊,那么存在且具有非零标题的日志就很热,并且其对应的数据库文件没有RESERVED锁。如果在文件日志中命名了超级期刊,那么如果文件日志的超级期刊存在且相应数据库文件上没有RESERVED锁,则该文件日志会很热。重要的是要了解什么时候期刊很热,这样前面的规则将以项目符号的形式重复:

4.1处理热门期刊

从数据库文件读取之前,SQLite始终检查该数据库文件是否具有热日志。如果文件确实有热日志,则在读取文件之前会回滚日志。这样,我们确保在读取数据库文件之前,该文件处于一致状态。

当一个进程想要从数据库文件中读取时,它遵循以下步骤序列:

  1. 打开数据库文件并获得共享锁。如果无法获得SHARED锁定,请立即失败并返回SQLITE_BUSY。
  2. 检查数据库文件是否具有热日志。如果该文件没有热日志,我们就完成了。立即返回。如果存在热日志,则必须通过此算法的后续步骤来回滚该日志。
  3. 在数据库文件上获取PENDING锁,然后获得EXCLUSIVE锁。(注意:不要获取RESERVED锁,因为这会使其他进程认为该日志不再很热。)如果我们未能获取这些锁,则意味着另一个进程已经在尝试回滚。在这种情况下,请丢弃所有锁,关闭数据库,然后返回SQLITE_BUSY。
  4. 阅读日志文件并回滚更改。
  5. 等待将回滚的更改写入持久性存储中。这样可以在发生另一次电源故障或崩溃的情况下保护数据库的完整性。
  6. 删除日志文件(如果设置了PRAGMA journal_mode = TRUNCATE,则将日志的长度截断为零字节;如果设置了PRAGMA journal_mode = PERSIST,则将日志的标题 截断为零)。
  7. 如果安全的话,请删除超级新闻文件。此步骤是可选的。这样做只是为了防止陈旧的超级新闻混乱磁盘驱动器。有关详细信息,请参见下面的讨论。
  8. 放下EXCLUSIVE和PENDING锁,但保留SHARED锁。

上面的算法成功完成后,可以安全地从数据库文件中读取。一旦完成所有读取,就将删除SHARED锁定。

4.2删除陈旧的超级新闻

过时的超级新闻是不再用于任何事物的超级新闻。无需删除过时的超级新闻。这样做的唯一原因是释放磁盘空间。

如果没有单独的文件日志指向超级期刊,那么该期刊就是陈旧的。为了弄清楚超级期刊是否陈旧,我们首先阅读了超级期刊以获取其所有文件日志的名称。然后,我们检查每个文件日志。如果在超级期刊中命名的任何文件日志都存在并指向该超级期刊,则该超级期刊就不会陈旧。如果所有文件日志都丢失或没有引用其他超级期刊,或者根本没有超级期刊,那么我们正在测试的超级期刊是陈旧的,可以安全地删除。

5.0写入数据库文件

要写入数据库,进程必须首先获得如上所述的SHARED锁(如果存在热日志,则可能回滚不完整的更改)。获取共享锁后,必须获取保留锁。RESERVED锁表示该进程打算在将来某个时刻写入数据库。一次只有一个进程可以持有RESERVED锁。但是,在保留RESERVED锁的同时,其他进程可以继续读取数据库。

如果要写入的进程无法获得RESERVED锁,则必须表示另一个进程已经具有RESERVED锁。在这种情况下,写入尝试将失败并返回SQLITE_BUSY。

在获得RESERVED锁之后,要编写的进程将创建一个回滚日志。日志的标题用数据库文件的原始大小初始化。尽管超级期刊名称最初是空的,但日志头中的空间也保留给超级期刊名称。

在更改数据库的任何页面之前,该过程将该页面的原始内容写入回滚日志。对页面的更改首先保存在内存中,而不写入磁盘。原始数据库文件保持不变,这意味着其他进程可以继续读取数据库。

最终,写入过程将要更新数据库文件,这是因为其内存缓存已满或准备好提交其更改。在此之前,编写者必须确保没有其他进程正在读取数据库,并且回滚日志数据安全地位于磁盘表面上,以便在断电时可以将其用于回滚不完整的更改。步骤如下:

  1. 确保所有回滚日志数据实际上已写入磁盘表面(而不仅仅是保留在操作系统或磁盘控制器缓存中),这样,如果发生电源故障,在恢复电源后数据仍将存在。
  2. 在数据库文件上获得PENDING锁,然后获得EXCLUSIVE锁。如果其他进程仍具有SHARED锁,则编写器可能必须等到这些SHARED锁清除后才能获得EXCLUSIVE锁。
  3. 将当前保存在内存中的所有页面修改写出到原始数据库磁盘文件中。

如果写入数据库文件的原因是由于内存缓存已满,则写入器将不会立即提交。而是,作者可能会继续对其他页面进行更改。在随后的更改写入数据库文件之前,必须将回滚日志再次刷新到磁盘。还要注意,在提交所有更改之前,必须保留编写者最初为了写入数据库而获得的EXCLUSIVE锁。这意味着从内存缓存第一次溢出到磁盘直到事务提交为止,没有其他进程能够访问数据库。

当编写者准备提交其更改时,它将执行以下步骤:

  1. 获得对数据库文件的独占锁定,并确保所有内存更改已使用上述步骤1-3的算法写入数据库文件。
  2. 将所有数据库文件更改刷新到磁盘。等待这些更改实际写入磁盘表面。
  3. 删除日志文件。(或者,如果PRAGMA journal_mode为TRUNCATE或PERSIST,则分别截断日志文件或将日志文件的标头设置为零。)这是提交更改的瞬间。在删除日志文件之前,如果发生电源故障或崩溃,则打开数据库的下一个过程将看到它具有热日志,并将回滚所做的更改。删除日志后,将不再有热日志,并且更改将保留。
  4. 从数据库文件中删除EXCLUSIVE和PENDING锁。

一旦从数据库文件中释放了PENDING锁,其他进程就可以再次开始读取数据库。在当前的实现中,还释放了RESERVED锁,但这对于正确操作不是必需的。

如果事务涉及多个数据库,则使用更复杂的提交序列,如下所示:

  1. 确保所有单个数据库文件都具有EXCLUSIVE锁和有效的日志。
  2. 创建一个超级新闻。超级新闻的名称是任意的。(当前实现将随机后缀附加到主数据库文件的名称,直到找到以前不存在的名称。)用所有单个日志的名称填充超级期刊,并将其内容刷新到磁盘。
  3. 将超级期刊的名称写入所有单个日志中(为此目的在各个日志的标题中留出的空间中),然后将各个日志的内容刷新到磁盘上,并等待这些更改到达磁盘表面。
  4. 将所有数据库文件更改刷新到磁盘。等待这些更改实际写入磁盘表面。
  5. 删除超级新闻文件。这是提交更改的瞬间。在删除超级日志文件之前,如果发生电源故障或崩溃,则单个文件日志将被认为是热门文件,并且在尝试读取它们的下一个过程中将被回滚。删除超级期刊后,文件日志将不再被视为热点,并且更改将继续存在。
  6. 删除所有单个日记文件。
  7. 从所有数据库文件中删除EXCLUSIVE和PENDING锁。

5.1作家饥饿

在SQLite版本2中,如果许多进程正在从数据库中读取数据,则可能是永远不会有没有活动的读取器的情况。而且,如果数据库上始终至少有一个读锁,则任何进程都将无法对数据库进行更改,因为不可能获得写锁。这种情况称为作家饥饿

SQLite版本3试图通过使用PENDING锁来避免编写者饥饿。PENDING锁允许现有的读取器继续运行,但阻止新的读取器连接到数据库。因此,当进程要写入繁忙的数据库时,可以设置PENDING锁,这将阻止新的读取器进入。假设现有读取器最终完成,则所有SHARED锁最终都将被清除,并且写入器将有机会使其处于运行状态。变化。

6.0如何损坏数据库文件

寻呼机模块非常强大,但是可以被颠覆。本节试图识别和解释风险。(另请参见“原子提交”一文中的“可能出错的部分”部分。

显然,硬件或操作系统故障会将错误的数据引入数据库文件或日志的中间会引起问题。同样,如果流氓进程打开数据库文件或日志并将格式错误的数据写入其中,则数据库将损坏。这些问题没有太多可做的,因此不再给予关注。

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()。作者无法验证任何这些报告。但是,如果它们为真,则意味着在意外断电后可能会损坏数据库。

如果 在/ etc / fstab中不带“ barrier = 1”选项的情况下挂载Linux ext3文件系统, 并且启用了磁盘驱动器写缓存,则在断电或操作系统崩溃后,文件系统可能会损坏。是否会发生损坏取决于磁盘控制硬件的详细信息。廉价的消费级磁盘更容易造成损坏,而对于具有高级功能(如非易失性写缓存)的企业级存储设备而言,问题就更少了。各种ext3专家都 证实了这种现象。我们被告知,大多数Linux发行版不使用barrier = 1,并且不禁用写缓存,因此大多数Linux发行版都容易受到此问题的影响。请注意,这是一个操作系统和硬件问题,SQLite无法解决该问题。 其他数据库引擎也遇到了同样的问题。

如果发生崩溃或电源故障并导致日志变热,但是该日志被删除,则下一个打开数据库的进程将不会知道该数据库包含需要回滚的更改。回滚将不会发生,并且数据库将处于不一致状态。回滚日志可能由于多种原因而被删除:

上面的最后(第四个)项目符号值得补充。当SQLite在Unix上创建日记文件时,它将打开包含该文件的目录并在目录上调用fsync(),以将目录信息推入磁盘。但是,假设其他一些过程是在断电时向包含数据库和日志的目录中添加或删除不相关的文件。可能与该其他过程无关的操作可能导致日记文件从目录中删除,并移至“丢失+找到”状态。这种情况不太可能发生,但有可能发生。最好的防御方法是使用日志文件系统,或者将数据库和日志单独保存在目录中。

对于涉及多个数据库和超级期刊的提交,如果各种数据库位于不同的磁盘卷上,并且在提交期间发生电源故障,则在计算机恢复备份时,可能会使用不同的名称重新装入磁盘。否则可能根本无法挂载某些磁盘。发生这种情况时,单个文件日志和超级期刊可能无法相互找到。这种情况下最糟糕的结果是,提交不再是原子的。某些数据库可能会回滚,而其他数据库可能不会。所有数据库将继续保持一致。为防止出现此问题,请在断电后将所有数据库保留在相同的磁盘卷上和/或使用完全相同的名称重新装入磁盘。

7.0 SQL级别的事务控制

SQLite版本3中对锁定和并发控制的更改还对事务在SQL语言级别上的工作方式进行了一些细微的更改。默认情况下,SQLite版本3在自动提交模式下运行。在自动提交模式下,对数据库的所有更改将在与当前数据库连接关联的所有操作完成后立即提交。

SQL命令“ BEGIN TRANSACTION”(TRANSACTION关键字是可选的)用于使SQLite退出自动提交模式。请注意,BEGIN命令不会在数据库上获得任何锁。在执行BEGIN命令之后,将在执行第一个SELECT语句时获取SHARED锁定。当执行第一个INSERT,UPDATE或DELETE语句时,将获得RESERVED锁。直到内存高速缓存已满并且必须溢出到磁盘或事务提交之前,才会获得EXCLUSIVE锁定。这样,系统将阻止对文件文件的读取访问延迟到最后一个可能的时刻。

SQL命令“ COMMIT”实际上并不将更改提交到磁盘。它只是重新打开自动提交。然后,在命令结束时,常规自动提交逻辑将接管并导致对磁盘的实际提交。SQL命令“ ROLLBACK”也可以通过重新打开自动提交来进行操作,但是它还设置了一个标志,该标志告诉自动提交逻辑进行回滚而不是提交。

如果SQL COMMIT命令打开了自动提交功能,然后自动提交逻辑尝试提交更改,但是由于某些其他进程持有SHARED锁定而失败,则自动提交功能将自动关闭。这样,在SHARED锁有机会清除后,用户可以在以后的时间重试COMMIT。

如果对同一SQLite数据库连接同时执行多个命令,则自动提交将推迟到最后一个命令完成时再执行。例如,如果正在执行SELECT语句,则命令的执行将在返回结果的每一行时暂停。在此暂停期间,可以对数据库中的其他表执行其他INSERT,UPDATE或DELETE命令。但是这些更改都不会提交,直到原始的SELECT语句完成为止。