Sqlite3 介绍

该文档最初是在2004年初创建的,SQLite版本2仍在广泛使用,并被编写为将SQLite版本3的新概念引入已经熟悉SQLite版本2的读者。但是现在,这个文档的大多数读者可能从来没有见过SQLite版本2,只熟悉SQLite版本3。然而,该文档继续作为数据库文件锁定如何在SQLite版本3中工作的权威参考。
该文档仅描述了旧回滚模式事务机制的锁定。单独描述新的提前写入日志或WAL模式。

1.0 SQLite版本3中的文件锁定和并发
SQLite版本3.0.0引入了一种新的锁定和日志机制,旨在提高SQLite版本2的并发性并减少写入器饥饿问题。新机制还允许涉及多个数据库文件的事务的原子提交。该文件描述了新的锁定机构。预期的读者是想要理解和/或修改寻呼机代码和审计员工作的程序员,以验证SQLite版本3的设计。

2.0 概述
锁定和并发控制由寻pager模块来处理。pager模块负责制作SQLite“ACID”(Atomic,一致性、隔离性和持久性)。pager模块确保一次发生变化,要么发生所有的更改,要么不执行任何更改,两个或多个进程不同时尝试以不兼容的方式访问数据库,并且一旦写入了更改,它们将持续存在,直到显式删除。pager还提供磁盘文件的一些内容的内存缓存。
pager与B-Trees,、文本编码、索引等的细节无关。从pager的角度来看,数据库由统一大小的块的单个文件组成。每个块被称为“页面”,通常大小为1024字节。页数从1开始。因此,数据库的前1024个字节被称为“第1页”,而第二个1024字节被称为“第2页”等等。所有其他编码细节都由库的更高层处理。pager使用几个模块(例如:OSUIX.C,OS.Wun.C)与操作系统通信,为操作系统服务提供统一的抽象。
pager模块有效地控制对单独线程或单独进程或两者的访问。在整个文档中,每当“进程”一词被写入时,你可以在不改变语句的真实性的情况下替换“线程”一词。

3.0 Locking

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

UNLOCKED 在数据库上保存。数据库既不读也不写。任何内部缓存的数据都被认为是可疑的,并在使用之前对数据库文件进行验证。其他进程可以读取或写入数据库,因为它们自己的锁定状态允许。这是默认状态。

SHARED 数据库可以读取但不写入。任何进程都可以同时持有共享锁,因此可以同时拥有许多读者。但是,当一个或多个共享锁处于活动状态时,不允许其他线程或进程写入数据库文件。

RESERVERD 预留的锁意味着该进程计划在将来某个时间点写入数据库文件,但它目前正从文件中读取。只有一个预留锁可以同时激活,尽管多个共享锁可以与单个预留锁共存。保留不同于未决,因为在保留锁的情况下,可以获取新的共享锁。

PENDING 意味着持有锁的进程希望尽快写入数据库,而只是等待所有当前共享锁来清除,从而获得独占锁。如果挂起的锁是活动的,则不允许新的共享锁,尽管允许现有的共享锁继续。

EXCLUSIVE 为了写入数据库文件,需要一个排他锁。文件中只允许一个专用锁,并且任何类型的其他锁都不允许与排他锁共存。为了最大化并发性,SQLite工作以最小化独占锁的占用时间。

操作系统接口层理解并跟踪上述五种锁定状态。pager模块仅跟踪五个锁定状态中的四个。PENDING始终只是通往EXCLUSIVE的路径上的临时垫脚石,因此pager模块不跟踪挂起的锁。

4回滚日志

当一个进程想要改变一个数据库文件(并且它不是在WAL模式)时,它首先在回滚日志中记录原始不变的数据库内容。回滚日志是一个普通的磁盘文件,它总是位于与数据库文件相同的目录或文件夹中,并与数据库文件的名称相同,并添加了一个日志后缀。回滚日志还记录数据库的初始大小,以便如果数据库文件增长,则可以在回滚中截断其原始大小。

如果SQLite同时使用多个数据库(使用附加命令),那么每个数据库都有自己的回滚日志。但也有一个单独的聚合日志称为主日志。主日志不包含用于回滚更改的页数据。相反,主日志包含每个所附数据库的单个数据库回滚日志的名称。每个单独的数据库回滚日志也包含主日志的名称。如果没有附加的数据库(或者如果没有附加的数据库参与当前事务),则不创建主日志,并且正常回滚日志包含通常保留的用于记录主日志名称的位置的空字符串。

回滚日志被认为是hot的,如果它需要回滚以恢复其数据库的完整性。当一个进程处于数据库更新的中间,一个程序或操作系统崩溃或电源故障阻止更新完成时,创建一个hot日志。Hot journals是一个例外情况 hot日志存在于从崩溃和电源故障中恢复。如果一切正常工作(也就是说,如果没有崩溃或电源故障),你将永远不会得到hot日记。

如果不涉及主日志,则如果存在日志,则日志是hot的,并且具有非零值报头,并且相应的数据库文件没有预留的锁。如果主日志在文件日志中被命名,则如果其主日志存在且相应的数据库文件上没有预留锁,则该文件日志是hot的。重要的是要理解什么时候journal 是hot,所以前面的规则将在子弹中重复:

一个 journal 是 hot 如果..
它存在,并且

它的大小大于512字节,并且

日志页眉为非零且格式良好,

它的主日志存在,或者主日志名称是空字符串,

在相应的数据库文件上没有预留的锁。

4.1 分配 hot journals

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

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

打开数据库文件并获得共享锁。如果无法获得共享锁,则立即失败并返回SQLITE_BUSY。

检查数据库文件是否有热日志。如果文件没有hot日志,我们就完成了。立即返回。如果存在hot日志,则该日志必须由该算法的后续步骤回滚。

获取PENDING锁,然后对数据库文件进行EXCLUSIVE锁。(注意:不要获取预留的锁,因为这会使其他进程认为日志不再是hot的。)如果我们无法获取这些锁,则意味着另一个进程已经尝试进行回滚。在这种情况下,删除所有锁,关闭数据库,并返回SQLITE_BUSY。

读取日志文件并回滚更改。

等待回滚的更改写入持久存储。这保护了数据库的完整性,以防发生另一次电源故障或崩溃。

如果设置了PrimaJournal模式=TrunTATE,则删除日志文件(或将日志截断为0个字节,如果设置了PrimaJournalObjult=持久性,则为零)。

如果这样做是安全的,则删除主日志文件。此步骤是可选的。这里只是为了防止陈旧的主日志凌乱磁盘驱动器。详情请参阅下面的讨论。

删除独占和挂起的锁,但保留共享锁。

在上述算法成功完成后,从数据库文件中读取是安全的。一旦读取完成,共享锁就被删除。

4.2删除陈旧的原版journals

过时的主日志是一个不再用于任何东西的主日志。没有过时的主期刊被删除的要求。这样做的唯一原因是释放磁盘空间。
如果没有单独的文件期刊指向它,则主日记是过时的。要想知道主日志是否过时,我们首先阅读主日志以获取其所有文件日志的名称。然后我们检查那些文件日志。如果主日志中的任何一个文件日志存在并指向主日志,则主日志不过时。如果所有的文件日志都丢失或引用其他主期刊或根本没有主日志,那么我们正在测试的主日志是陈旧的,可以安全地删除。

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

如果想要写入的进程无法获得一个RESERVED锁,它必须意味着另一个进程已经有一个RESERVED锁。在这种情况下,写入尝试失败并返回SQLITE_BUSY。

获得一个RESERVED锁后,想要写入的进程创建一个回滚日志。日志的标题用数据库文件的原始大小初始化。日记首部中的空间也为主日记名称保留,但主日记名称最初为空。

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

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

确保所有回滚日志数据实际上已写入磁盘表面(并且不仅仅保存在操作系统或磁盘控制器缓存中),以便在发生电源故障时,恢复供电后数据仍然存在。
获取PENDING锁,然后获取数据库文件的EXCLUSIVE锁。如果其他进程仍然具有SHARED锁定,那么作者可能必须等到SHARED锁定清除,然后才能获得EXCLUSIVE锁定。
将当前保存在内存中的所有页面修改写入原始数据库磁盘文件。
如果写入数据库文件的原因是因为内存缓存已满,那么作者将不会立即提交。相反,作者可能会继续对其他页面进行更改。在将后续更改写入数据库文件之前,必须将回滚日志再次刷新到磁盘。还要注意,写入器为了写入数据库而获得的EXCLUSIVE锁必须保持到所有更改均已提交为止。这意味着在事务提交之前,没有其他进程能够从内存缓存首次溢出到磁盘时访问数据库。

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

获取数据库文件的EXCLUSIVE锁,并确保使用上述步骤1-3的算法将所有内存更改写入数据库文件。
将所有数据库文件更改清理到磁盘。等待这些更改实际写入磁盘表面。
删除日志文件。 (或者,如果PRAGMA journal_mode是TRUNCATE或PERSIST,则分别截断日志文件或归档日志文件的标头。)这是提交更改的时刻。在删除日志文件之前,如果发生电源故障或崩溃,下一个打开数据库的进程将看到它有一个热日志,并将更改回滚。该期刊被删除后,将不再有热门期刊,更改将持续。
从数据库文件中删除EXCLUSIVE和PENDING锁。
一旦PEND​​ING锁从数据库文件中释放,其他进程就可以再次开始读取数据库。在当前的实现中,保留锁也被释放,但这对于正确的操作不是必需的。

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

确保所有单个数据库文件都有一个EXCLUSIVE锁和一个有效的日志。
创建一个主日志。主日志的名称是任意的。 (当前的实现将随机后缀附加到主数据库文件的名称上,直到它找到一个先前不存在的名称。)使用所有单独日志的名称填充主日志并将其内容刷新到磁盘。
将主日志的名称写入所有单独的日志(在各个日志的标题中为此专门设置的空间中),并将各个日志的内容刷新到磁盘,并等待这些更改到达磁盘表面。
将所有数据库文件更改清理到磁盘。等待这些更改实际写入磁盘表面。
删除主日志文件这是变更实施的时刻。 在删除主日志文件之前,如果发生电源故障或崩溃,单个文件日志将被视为热点,并在下一个尝试读取它们的进程中回滚。 在主日志被删除后,文件日志将不再被视为热点,并且更改将持续。
删除所有单独的日记文件。
从所有数据库文件中删除EXCLUSIVE和PENDING锁。

5.1作家饥饿
在SQLite版本2中,如果有许多进程正在从数据库中读取数据,那么可能会出现这样的情况,即没有活跃的读者没有时间。 如果数据库中始终有至少一个读锁定,则任何进程都无法对数据库进行更改,因为无法获取写锁定。 这种情况被称为作家饥饿。

SQLite版本3试图通过使用PENDING锁来避免编写器的匮乏。 PENDING锁允许现有的阅读器继续,但阻止新的阅读器连接到数据库。 所以当一个进程想要编写一个繁忙的数据库时,它可以设置一个PENDING锁,这将阻止新读者进入。假设现有的读者最终完成,所有SHARED锁最终将清除,作者将有机会使其 变化。

6.0如何破坏数据库文件
寻呼机模块非常强大,但可以被颠覆。本节试图识别和解释风险。 (另见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无法抵御的硬件和/或操作系统错误。

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

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

管理员可能在操作系统崩溃或电源故障后进行清理,请查看日志文件,认为它是垃圾文件,然后将其删除。
有人(或某个进程)可能会重命名数据库文件,但无法重命名其关联的日志。
如果数据库文件具有别名(硬链接或软链接),并且该文件是用与创建日记帐不同的别名打开的,则不会找到日记帐。为了避免这个问题,你不应该创建链接到SQLite数据库文件。
电源故障后文件系统损坏可能会导致日志被重命名或删除。
上面的最后一个(第四个)符号值得补充评论。当SQLite在Unix上创建日志文件时,它会打开包含该文件的目录并在该目录上调用fsync(),以便将目录信息压入磁盘。但是,假设其他进程在断电时将不相关的文件添加到或删除包含数据库和日志的目录。这个其他过程的所谓不相关的行为可能会导致日志文件从目录中删除,并进入“lost + found”。这是不太可能的情况,但可能会发生。最好的防御措施是使用日志文件系统或将数据库和日志本身保存在目录中。

对于涉及多个数据库和主日志的提交,如果各种数据库位于不同的磁盘卷上并且在提交期间发生电源故障,那么当机器恢复时,可能会用不同的名称重新装入磁盘。或者根本不可能安装某些磁盘。发生这种情况时,单个文件日志和主日志可能无法找到对方。 这种情况最糟糕的结果是提交不再是原子的。 有些数据库可能会回滚,其他数据库则可能不会。 所有数据库将继续保持一致。 为了防范这个问题,请在断电后使用完全相同的名称将所有数据库保留在同一个磁盘卷上和/或重新装入磁盘。

7.0在SQL级别的事务控制
SQLite版本3中对锁定和并发控制的更改还引入了事务处理SQL语言级别方式的一些细微更改。默认情况下,SQLite版本3以自动提交模式运行。在自动提交模式下,只要与当前数据库连接相关的所有操作完成,就立即提交对数据库的所有更改。

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

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

如果SQL COMMIT命令打开自动提交并且自动提交逻辑然后尝试提交更改但由于其他进程持有SHARED锁而失败,则自动提交将自动关闭。这允许用户在SHARED锁有机会清除后稍后重试COMMIT。

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值