SQLite的原子提交--单文件场景

3. 单文件提交

我们首先概要说明SQLite在单个数据库文件上为了执行事务的原子提交而采取的步骤.在后面的部分将讨论如何设计文件格式以保护其在断电故障中损坏,以及原子提交在多个数据库上的执行.

3.1. Initial State

数据库连接首次打开的时候, 计算机的状态如下图所示.图中最右边标记为Disk的区域为大容量存储设备中的信息,每个矩形为一个扇区,蓝颜色代表扇区中的原始数据.中间区域是操作系统的磁盘cache.此时,cache是冷的, 没有缓存任何数据.左边区域代表SQLite进程空间中的内容,由于SQLite连接处于刚打开状态,因此用户空间还没有数据. 

3.2. Acquiring A Read Lock

在对SQLite写操作之前,即使是追加写操作, 也必须先读取数据库检查其状态.为了知道如何解析INSERT语句以及数据存储的位置,SQLite必须读取sqlite_master 表格中的schema信息.

读取数据库文件的第一步是获取该文件的一个共享锁(shared lock).共享锁允许该数据库文件的多个连接同时进行读取操作,但是共享锁将阻止该数据库文件连接上的写入操作.如果另一个连接正在写入该数据库文件, 读操作可能读取到的数据中,部分是写入操作之前的,部分数据是写入操作之后的, 这将造成写入操作是非原子性的, 因此有必要在共享锁期间禁止其他连接上的写入操作.

需要注意的是,共享锁存在于操作系统的磁盘cache中,而不是在磁盘上,文件锁通常仅仅是操作系统内核中的标识,其执行细节取决于操作系统提供的接口层.因此如果操作系统崩溃或者发生断电故障,锁将立即消失.如果创建锁的进程退出,锁也将立即消失. 
这里写图片描述

3.3. Reading Information Out Of The Database

获得共享锁以后,我们就可以从数据库中读取数据了.此时,我们假设cache是冷的,因此数据首先从大容量存储设备读取到操作系统cache中,然后在从操作系统cache中读取到用户空间.在后续的读取操作中,需要的数据可能已经存在于操作系统cache中,因此只需从操作系统cache中读取数据到用户空间.

通常,读取操作会从数据库文件中读取部分页(pages).在这个图示的例子中,数据库文件有8个页, 读取操作获得了其中3个页中的数据.在一个典型的应用程序中,数据库文件可能有几千个页,读取操作通常只访问其中一小部分页. 
这里写图片描述

3.4. Obtaining A Reserved Lock

在对SQLite数据库文件进行写操作之前,必须获得一个预订锁(reserved lock).预订锁和共享锁一样,都允许其他进程读取数据库文件.一个预订锁可以和其他进程的多个共享锁共存,但是,一个数据库文件只有一个预订锁.因此在给定时间内,只有一个进程允许对数据库文件进行写操作.

预订锁的设计思想是,一个进程将要对数据库文件进行修改,但此时还没有发起修改请求.因为修改请求还没有开始,其他进程可以继续进行读取操作,然而,该锁将禁止其他进程的写入操作. 
这里写图片描述

3.5. Creating A Rollback Journal File

在对数据库文件进行任何修改之前,SQLite首先创建一个分离的回滚日志文件,并且把被修改数据的原始内容页面写入到该日志文件中.回滚日志文件的设计思想是,其包含数据库文件恢复到原始状态的所有数据.

回滚日志文件包含一个小的文件头(图示中绿色区域),用来记录文件库文件的原始大小.回滚日志文件现在只在操作系统的磁盘cache中创建,在后续的某个时刻将被写入到大容量存储设备上去.因为没有发生实际的IO操作,所以创建过程很快.这一阶段的操作如下图所示. 
这里写图片描述

3.6. Changing Database Pages In User Space

在原始页面数据保存到回滚日志文件以后, SQLite进程就可以修改其页面中的数据了.每个数据库连接有其私有的用户空间,因此不同数据库连接间的数据更改是不可见的.其他的数据库连接仍然能看看到操作系统磁盘cache中还没有修改的数据.因此,即使一个进程正在修改其用户空间中的数据, 其他进程仍然能够读取操作系统磁盘cache中的原始数据. 
这里写图片描述

3.7. Flushing The Rollback Journal File To Mass Storage

接下来的步骤就是刷新(flush)回滚日志文件中的数据到非易失存储设备.正如我们后续将看到的,这一步骤至关重要,它将保证数据库在异常断电故障时,数据不被损坏.因为写磁盘通常是一个耗时操作,这个操作步骤当然也需要很长时间.

这个步骤通常比简单的刷新回滚日志文件到磁盘更复杂一些.在大多数操作系统上, 需要进行2个分离的刷新(或者fsync)操作.第一次刷新将写入回滚日志文件的基本数据,然后更新回滚日志文件头中的页面号,这些页面中的数据就是回滚日志的基本内容.第二次刷新将把更改的回滚日志头写入到磁盘.关于为什么需要更新回滚日志头数据并在第二次刷新中写入到磁盘的细节,后续部分再讨论. 
这里写图片描述

3.8. Obtaining An Exclusive Lock

在修改磁盘上的数据库文件之前,我们必须获得该数据库的一个排他锁( exclusive lock).获得排他锁其实是一个两阶段的处理过程,首先, SQLite获得一个”pending”锁,然后升级pending锁到排他锁.

pending锁允许其他获得共享锁的进程继续读取数据库文件,但是它将阻止创建新的共享锁.pending锁的设计思想是,防止由于大量的读操作导致的写操作饿死.由于可能存在几十或者几百个进程读取数据库文件,每个进程在读取之前获得共享锁, 然后读取数据库文件,最后释放共享锁.如果大量的进程读取相同的数据库文件,在之前的进程没有读取完毕时, 新进程又获得共享锁,因此数据库文件上将始终存在共享锁,写操作的进程没有机会获得排他锁.pending锁将允许已经存在的共享锁继续操作,但是将阻止创建新的共享锁.最终,所有的共享锁都将退出,pending锁有机会升级到排他锁. 
这里写图片描述

3.9. Writing Changes To The Database File

一旦获得排他锁, 就意味着没有其他进程正在进行读取操作,这时候对数据库文件的写操作是安全的.通常,写请求中的数据只是更新操作系统磁盘cache,并没有真正写到大容量存储设备上. 
这里写图片描述

3.10. 0 Flushing Changes To Mass Storage

为了将写操作中的更新数据真正写入到数据库文件, 这里需要刷新操作.刷新操作至关重要,因为它将保证数据库文件在断电故障时不被损坏.和3.7节中刷新回滚日志文件一样,这是个耗时操作,SQLite事务提交中的大部分时间都将消耗在磁盘IO的刷新操作中. 
这里写图片

3.11. 1 Deleting The Rollback Journal

写请求中的数据安全的存储到大容量存储设备以后,回滚日志文件就可以删除了.在事务提交过程中, 回滚日志文件删除是瞬态完成的.如果在回滚日志文件被删除之前,发生了操作系统崩溃或者断电故障,恢复程序(后面将介绍)将认为数据库文件没有任何改变.如果在回滚日志文件被删除之后,发生了操作系统崩溃或者断电故障,恢复程序将认为所有提交都已经写入了磁盘文件.因此根据回滚日志文件是否存在, SQLite可以判断事务提交中的数据要么全部写入了磁盘文件,要么没有写入到磁盘文件.

从用户进程的角度看, 删除一个文件像是原子性的,但实际上并不是.用户进程总是可以通过操作系统判断一个文件是否存在.如果在事务提交过程中发生了断电故障, SQLite重启后将通过操作系统检查回滚日志文件是否存在,如果回滚日志文件存在,那么事务是没有完成的,将进行回滚操作.如果回滚日志文件不存在,则说明事务提交已经完成.

事务的存在取决于回滚日志文件是否存在,并且从用户进程角度看,删除一个文件是原子性的,因此,事务看起来也是原子性的.

在许多操作系统上,删除一个文件是比较昂贵的操作. SQLite会做优化处理,会把回滚日志文件大小截断为0字节,或者将回滚日志文件的头信息清零.通过其中的任意一种优化方法处理后,回滚日志文件都不能再用于回滚操作, 因此可以认为事务提交已经完成.从用户进程角度看,截断文件大小为0字节和删除一个文件一样是原子性的.尽管对回滚日志文件头进行清零操作不是原子性的,但是如果回滚日志文件头信息中有非法格式的数据,其都不能用于回滚操作,因此,我们可以认为,只要回滚日志文件头中有字段被改成无效值,事务提交都是已经完成状态.典型的,我们只要把回滚日志文件头中的第一个字节设置为0,该文件就变成无效了. 
这里写图片描述

3.12. 2 Releasing The Lock

提交阶段的最后一个步骤是释放排他锁, 以便其他进程可以访问该数据库文件.

在下面的图示中,当锁被释放以后,用户空间中的数据也被清除了.SQLite之前的老版本是这样执行的,但是在最近的版本中,将保留用户空间中的数据,这样在下一次事务中,这些数据可能会被重用.重用用户空间中的数据比从操作系统磁盘cache中读取数据,或者通过磁盘驱动从磁盘读取数据都要高效的多.在重用用户空间的数据之前,我们首先必须获得该文件的共享锁,并且确保没有其他进程修改过数据库文件.在数据库文件的第一个页面中,存在一个计数字段,每当数据库文件修改以后,这字段都将累加更系.我们可以检查这个字段来判断是否其他进程修改过数据库文件.如果数据库文件被其他进程修改了, 用户空间中的数据必须清除,并重新从操作系统读取.如果数据库没有被其他进程修改,重用用户进程空间中的数据将获得显著的性能提升. 
这里写图片描述

4. Rollback

原子提交被认为是瞬态完成的,但是之前描述的提交过程肯定是需要一定时间才能完成的.假定计算机的断电故障中断了提交过程,为了维持瞬态提交这种假象,我们必须不完整的更改,把数据库文件恢复到事务开始之前的状态.

4.1. When Something Goes Wrong…

假定在上面的步骤3.10中发生了断电故障,此时正在进行刷新写操作数据到磁盘.当重新上电以后,数据状态可能如下图所示.我们正试图向磁盘刷新三个页面的数据,但是仅有一个页面被成功写入,另一个页面部分写入,而第三个页面根本没有写入.

恢复上电以后,磁盘上的回滚日志文件是完整有效的.这一点至关重要,因为步骤3.7中的刷新操作绝对保证了在对数据库文件进行任何改动之前, 回滚日志文件已经安全的写入到了非易失存储设备中. 
这里写图片描述

4.2. Hot Rollback Journals

任何SQLite进程在访问数据库文件时,都需要获得共享锁(见3.2描述), 然后注意到存在一个会滚日志文件,SQLite进程检查该会滚日志文件是否是一个热日志(“hot journal”).如果该回滚日志文件是热日志文件,需要对其进行回放操作,以便恢复数据库文件到其原始状态.当之前的事务提交过程中发生了操作系统崩溃或者断电故障,才有可能存在一个热日志文件.

一个热日志文件需要满足下面的所有条件:

  • 回滚日志文件存在.
  • 回滚日志文件不是空的.
  • 主数据库文件上没有预订锁.
  • 回滚日志文件头格式正确,没有被清零.
  • 回滚日志文件不包含住日志文件名(见5.5中的描述),或者包含主日志文件名,且该主日志文件存在.

这里写图片描述

4.3. Obtaining An Exclusive Lock On The Database

处理热日志文件的第一步是获得数据库文件的排他锁,这将阻止其他进程对该热日志文件进行回滚操作. 
这里写图片描述

4.4. Rolling Back Incomplete Changes

一旦进程获得了排他锁,就允许对数据库文件进行写操作.它从回滚日志文件中读取原始的数据,然后把这些原始数据写到数据库文件相应的页面中.回滚日志文件头信息中记录着数据库文件在事务开始之前的原始大小,SQLite使用这个信息将数据库文件截断为原始文件大小以避免之前不完整的事务提交导致的数据库文件变大的情况.回滚操作完成后, 数据库文件应该和事务开始之前大小一致并且文件内容也一致. 
这里写图片描述

4.5. Deleting The Hot Journal

当回滚日志文件中的数据全部回放到数据库文件中后,并且已经刷新到磁盘,以避免我们遭遇另一次掉电故障,热日志文件就可以被删除了. 
正如3.11部分所讨论的,当操作系统执行文件删除操作比较昂贵时,作为优化措施, 回滚日志文件可能被截断为0字节长度,或者其头信息中可能被清零.无论哪种处理方式,回滚日志文件将不再是热日志文件. 
这里写图片描述

4.6. Continue As If The Uncompleted Writes Had Never Happened

恢复过程的最后一步是将排他锁降级到共享锁.至此,数据库文件就回到了事务提交之前的状态.因为所有的恢复处理都是原子性的和透明的, SQLite就好像从来没有发生过被终止的事务一样. 
这里写图片描述

 

https://blog.csdn.net/azurelaker/article/details/82594113

posted @ 2019-04-09 19:32  zzfx  阅读(459)  评论(0编辑  收藏  举报