SQLite File IO Specification
SQLite File IO Specification
Overview
SQLite将整个数据库存储在单个文件中,其格式在SQLite文件数据库文件格式
文档ff_sqlitert_requirements中进行了描述。每个数据库文件都存储在一个文件系统中,可能由主机操作系统提供。主机应用程序不需要直接与操作系统连接,而是需要提供一个实现SQLite虚拟文件系统
接口的适配器组件(在capi_sqlitert_requirements中进行了介绍)。适配器组件负责将SQLite所做的调用转换为VFS
接口,以调用操作系统提供的文件系统接口。这种安排如图figure_vfs_role所示。
图 - 虚拟文件系统(VFS)适配器
尽管设计一个使用VFS
接口来读取和更新文件系统中存储的数据库文件内容的系统会很容易,但这样的系统需要解决几个复杂的问题:
- 即使应用程序,操作系统或电源故障在更新数据库文件的过程中或稍后发生,SQLite也需要
实现原子事务和持久事务
(即ACID首字母缩略词中的'A'和'D')。为了在潜在的应用程序,操作系统或电源故障的情况下执行原子事务,数据库编写者在写入数据库文件之前,将他们要修改的数据库文件的那些部分的副本写入第二个文件(日志文件
) 。如果修改数据库文件时发生故障,SQLite可以基于日志文件
的内容重新构建原始数据库(在尝试修改之前)。
- SQLite需要
实现独立的事务
(ACID首字母缩略词中的'I')。
这是通过使用VFS适配器提供的文件锁定工具来完成序列化写入器(写入事务)并防止读取器(读取事务)访问数据库文件,而作者正在更新它们的过程中。
- 出于性能考虑,
最大限度地减少读取和写入
文件系统的数据量
。正如人们所期望的那样,通过在主内存中缓存部分数据库文件,从数据库文件中读取的数据量
被最小化。此外,作为同一个写入事务的
一部分的数据库文件的多次更新可以被缓存在主存储器中并一起写入该文件,从而允许更高效的IO模式并消除如果数据库的一部分可能发生的冗余写入操作文件在单个写入事务中
不止一次被修改。TODO:上述要点的系统要求参考。本文详细描述了SQLite使用VFS适配器组件提供的API来解决问题并实现上面列举的策略的方式。它还指定了有关VFS适配器提供访问权的系统属性的假设。例如,有关数据库文件更新时发生电源故障时可能发生的数据损坏程度的具体假设,请参见fs_characteristics一节。本文档未指定必须由VFS适配器组件实现的接口的详细信息,该信息留给capi_sqlitert_requirements。与涉及C-API要求的其他文档的关系:
- 打开连接。
- 关闭连接。
与SQL要求相关:
- 打开一个只读事务。
- 终止只读事务。
- 打开一个读写事务。
- 提交一个读写事务。
- 回滚一个读写事务。
- 打开一个声明事务。
- 提交一个声明事务。
- 回滚一个语句事务。
- 提交多文件事务。
与文件格式要求相关:
- 钉住(读取)数据库页面。
- 取消固定数据库页面。
- 修改数据库页面的内容。
- 将新页面附加到数据库文件。
- 截断数据库文件末尾的页面。
Document Structure
本文档的vfs_assumption部分描述了有关VFS适配器组件提供访问的系统的各种假设。介绍VFS实现所需的基本功能和功能,以及capi_sqlitert_requirements中对VFS接口的描述。通过更详细地描述本文档中介绍的算法所依赖的有关VFS实现的假设,vfs_assumptions部分对此进行了补充。其中一些假设与性能问题有关,但大多数涉及文件系统在修改数据库文件过程中发生故障后的预期状态。
Section database_connections引入了数据库连接
的概念,即用于访问数据库文件的文件句柄和内存中缓存的组合。它还介绍了创建(打开)新数据库连接
时以及何时销毁(关闭)时所需的VFS操作。
Section reading_data描述了打开读取事务
和从数据库文件读取数据所需的步骤。
Section_data部分描述了打开写入事务
并将数据写入数据库文件所需的步骤。
部分回滚描述了由于显式用户指令导致中止写入事务
回滚(还原)的方式,或者在SQLite处于中途更新数据库文件时发生应用程序,操作系统或电源故障的情况。
page_cache_algorithms部分描述了用于确定数据库文件的哪些部分由页面缓存缓存的
一些算法,以及它们对所需VFS操作的数量和性质的影响。在本文档中,首先看起来很奇怪的是页面缓存
,主要是一个实现细节。但是,有必要确认并描述页面缓存
,以便更全面地说明SQLite执行的IO的性质和数量。
Glossary
待办事项:准备好这份文件后,让词汇保持一致,然后在此处添加词汇表。
VFS Adaptor Related Assumptions
本节记录了有关VFS适配器提供访问权的系统的假设。fs_characteristics部分中提到的假设特别重要。如果这些假设不成立,那么电源或操作系统故障可能会导致SQLite数据库损坏。
Performance Related Assumptions
SQLite使用本节中的假设来尝试加速读取和写入数据库文件。
假定按顺序将一系列连续的数据块写入文件比以任意顺序写入相同的块要快。
System Failure Related Assumptions
在操作系统或电源故障的情况下,可用的文件系统软件和存储硬件的各种组合提供不同级别的保证,以确保在故障之前或故障期间写入文件系统的数据的完整性。为了安全地修改数据库文件,SQLite需要执行的IO操作的确切组合取决于目标平台的确切特性。
本节描述了SQLite关于电源或系统故障后文件系统内容的假设。换句话说,它描述了这种事件可能导致的文件和文件系统损坏的程度。
SQLite使用数据库文件file-handle的xDeviceCharacteristics()和xSectorSize()方法查询文件系统特性的实现。这两种方法只能在数据库文件上打开的文件句柄上调用。它们不被称为日志文件
,主日志文件
或临时数据库文件
。
通过调用xSectorSize()方法确定的文件系统扇区大小
值是512和32768之间的2的幂的幂,包括TODO:完全如何确定。SQLite假定底层存储设备将数据存储在每个扇区的扇区大小
字节块中。还假设每个文件的每个对齐的扇区大小的
字节块存储在单个设备扇区中。如果文件大小不是扇区大小
字节的精确倍数,则最终的设备扇区部分为空。
通常,SQLite假定如果在更新扇区的任何部分时发生电源故障,则在恢复后整个设备扇区的内容是可疑的。写入文件中某个扇区的任何部分后,假定修改后的扇区内容保存在系统内某个易失性缓冲区(主存储器,磁盘缓存等)中。SQLite不会假定更新的数据已经到达持久性存储介质,直到它通过调用VFS xSync()方法成功同步
相应的文件为止。同步
文件会导致对该文件的所有修改,直到该点被提交到持久性存储。
基于以上所述,SQLite是围绕文件系统的模型设计的,其中写入的文件的任何扇区都被认为处于临时状态,直到文件成功同步后
。如果在扇区处于瞬态状态时发生电力或系统故障,则在恢复后不可能预测其内容。它可以被正确写入,而不是被写入,被随机数据或其任何组合覆盖。
例如,如果给定文件系统的扇区大小
为2048字节,并且SQLite打开一个文件并写入一个1024字节的数据块来抵消文件的3072,则根据该模型,该文件的第二个扇区是在瞬态中。如果在下次调用文件句柄上的xSync()之前或期间发生电源故障或操作系统崩溃,则在系统恢复之后,SQLite会假定字节偏移2048和4095之间的所有文件数据都是无效的。它还假定由于文件的第一个扇区包含字节偏移0到2047(含)的数据是有效的,因为它在崩溃发生时不处于瞬态状态。
假设在电力或系统故障之后,瞬态中的任何和所有部分都可能被损坏,这是一种非常悲观的方法。一些现代系统提供比这更复杂的保证。SQLite允许VFS实现在运行时指定当前平台支持零个或多个以下属性:
- 该
安全-追加
属性。如果系统支持safe-append
属性,则意味着在扩展文件时,在更新文件本身的大小之前,将新数据写入永久介质。这保证了如果在文件扩展之后发生故障,在恢复之后,扩展文件的写入操作看起来成功或根本没有发生。无效或垃圾数据不可能出现在文件的扩展区域中。
- 该
原子写入
性能。一个支持这个属性的系统也指定了它能够写入的块的大小。有效大小是大于512的两个幂。如果写入操作修改了一个n
字节的块,其中n
是支持原子写入
的块大小之一,则不可能使n
字节的对齐写入导致数据损坏。如果在这样的写入操作之后并且在适用的文件句柄被同步
之前发生故障,那么在恢复之后,它将看起来好像写入操作成功或根本没有发生。不可能只有部分由写入操作指定的数据被写入永久介质,写入操作跨越的扇区的任何内容也不可能被垃圾数据替换,因为通常认为它是。
- 在
连续写入
性能。支持sequential-write
属性的系统保证对同一文件系统内文件的各种写入操作按照与应用程序执行的顺序相同的顺序写入
永久介质,并且每个操作在下一个操作之前结束开始。如果系统支持顺序写入
属性,那么用于确定故障发生后文件系统可能状态的模型是不同的。
如果系统支持顺序写入
,则假定在文件系统内同步
任何文件会将所有文件(而不仅仅是已同步
文件)的所有写入操作刷新到永久介质。如果发生故障,则不知道自上次文件同步
以来SQLite是否执行了任何写入操作。SQLite能够假设如果未知状态的写操作按照它们发生的顺序排列:
1. the first _n_ operations will have been executed successfully,
2. the next operation puts all device sectors that it modifies into the transient state, so that following recovery each sector may be partially written, completely written, not written at all or populated with garbage data,
3. the remaining operations will not have had any effect on the contents of the file-system.
Failure Related Assumption Details
本节描述父节中提出的假设如何应用于VFS到SQLite为修改文件系统内容而提供的各个API函数和操作。
SQLite使用以下四种操作类型组合操作文件系统的内容:
创建文件
操作。SQLite可以通过调用sqlite3_io_methods对象的xOpen()方法在文件系统内创建新文件。
删除文件
操作。SQLite可以通过调用sqlite3_io_methods对象的xDelete()方法来从文件系统中删除文件
。
截断文件
操作。SQLite可以通过调用sqlite3_file对象的xTruncate()方法来截断现有文件。
编写文件
操作。SQLite可以通过调用sqlite3_file对象的xWrite()方法修改内容并通过文件增加文件的大小。
此外,所有VFS实现都需要提供同步文件
操作,通过sqlite3_file对象的xSync()方法访问,用于将文件的创建,写入和截断操作刷新到永久存储介质。
本节中的形式化假设是指系统故障
事件。在这种情况下,这应该被解释为导致系统停止运行的任何故障。例如电源故障或操作系统崩溃。
SQLite不假定创建文件
操作实际上已经修改了永久性存储中的文件系统记录,直到文件成功同步之后
。
如果在“创建文件”操作期间或之后发生系统故障,但在创建文件同步
之前发生系统故障,则SQLite假定在系统恢复后创建的文件可能不存在。
当然,在系统恢复之后它也可能存在。
如果SQLite执行“创建文件”操作,然后创建的文件同步
,则SQLite会假定与“创建文件”操作对应的文件系统修改已被提交给持久性媒体。假定如果在文件成功同步
后任何时候发生系统故障,那么在系统恢复之后文件将保证出现在文件系统中。
甲删除文件
操作(通过向VFS xDelete()方法的调用被调用)被假定为是一个原子和耐用操作。
如果在“删除文件”操作(调用VFS xDelete()方法)后任何时候系统发生故障都会成功返回,则假定系统恢复后文件系统不包含已删除的文件。
如果在“删除文件”操作期间发生系统故障,则假定在系统恢复之后,文件系统将包含正在尝试操作之前的状态中删除的文件,或者根本不包含该文件。假定文件不可能由于在“删除文件”操作期间发生故障而纯粹被损坏。
假定截断文件
操作的效果不会被持续到相应的文件同步后
。
如果系统在“截断文件”操作期间或之后发生系统故障,但在截断文件同步之前
,SQLite会假定截断文件的大小要么大于大小,要么大于截断大小。
如果在“截断文件”操作期间或之后发生系统故障,但在截断文件同步
之前发生系统故障,则假定该文件的内容达到要截断文件的大小而不被损坏。
上述两个假设可能被解释为意味着如果在文件截断之后但在截断文件同步
之前发生系统故障,那么文件内容跟在截断点之后的内容可能不被信任。它们可能包含原始文件数据,或可能包含垃圾。
如果SQLite执行“截断文件”操作,然后截断的文件同步
,则SQLite会假定与“截断文件”操作相对应的文件系统修改已被提交到永久介质。假定如果在文件成功同步
后任何时候发生系统故障,则文件截断的影响将保证在恢复后出现在文件系统中。
甲写文件
操作修改所述文件系统内的现有文件的内容。它也可能增加文件的大小。写入文件
操作的效果不会被假定为在相应的文件同步
之后才会持久化。
如果在“写入文件
”操作期间或之后发生系统故障,但在相应文件同步
之前发生系统故障,则假定写入文件
操作所跨越的所有扇区的内容在系统恢复后不可信。这包括没有被写入文件
操作实际修改的扇区的区域。
如果支持的系统上发生系统故障原子写
为的大小的块属性Ñ
以下中的对齐的写入字节Ñ
字节到一个文件,但该文件已被成功地前同步
,则假定以下恢复所有扇区由跨越写入操作被正确更新,或者根本没有任何部分被修改。
如果在支持安全追加
的系统上发生系统故障,该系统在将数据追加到文件末尾而不修改任何现有文件内容但在文件成功同步
之前执行的写入操作之后发生系统故障,数据正确地附加到文件,或文件大小保持不变。这种情况是假定文件不可能被扩展但是填充了不正确的数据下发生的。
在系统恢复之后,如果设备扇区被认为是A21008定义的不可信任的,并且A21011或A21012都不适用于所写入的字节范围,则不能假定恢复后扇区的内容。假定这样一个扇区可以正确写入,而不是写入,则可以填充垃圾数据或其任何组合。
如果在导致文件增长的“写入文件”操作期间或之后发生系统故障,但是在相应文件同步
之前发生系统故障,则假定恢复后的文件大小大于或大于当它最近被同步时
。
如果系统支持顺序写入
属性,则可以在从系统故障
恢复之后针对文件系统的状态作出进一步的假设。具体来说,假定创建,截断,删除和写入文件操作以与SQLite执行的顺序相同的顺序应用于持久化表示。此外,假定文件系统等待,直到在下一次尝试之前将一个操作安全地写入永久介质,就好像在每次操作之后同步
相关文件一样。
如果在支持sequential-write
属性的系统上发生系统故障,则假定在最后一次同步
任何文件之前完成的所有操作都已成功提交到永久介质。
如果在支持sequential-write
属性的系统上发生系统故障,则假定文件系统可能处于后续恢复状态的一组可能状态与每个写入操作从最最近的一个文件时间同步
是本身后跟一个同步文件
操作,以及系统故障可能在任何写或已发生同步文件
操作。
Database Connections
在本文档中,术语数据库连接
与人们可能假设的含义略有不同。由sqlite3_open()
和sqlite3_open16()
API(TODO:reference)返回的句柄被称为数据库句柄
。一个数据库连接
是使用一个单一的文件句柄,这是保持打开的连接的生存期,以一个单一的数据库文件的连接。使用SQL ATTACH语法,可以通过单个数据库句柄
访问多个数据库连接
。或者,使用SQLite的共享缓存模式
功能,多个数据库句柄
可以访问单个数据库连接
。
通常,只要用户在真实数据库文件(而非内存数据库)上打开新的数据库句柄
,或者使用SQL ATTACH语法将数据库文件附加到现有数据库连接
时,就会打开一个新的数据库连接
。但是,如果启用了共享高速缓存模式
功能,则可以通过现有数据库连接
访问数据库文件。有关共享高速缓存模式的
更多信息,请参阅TODO:参考。本文档的open_new_connection部分详细介绍了打开新连接所需的各种IO操作。
同样,数据库连接
,当用户关闭通常是关闭数据库句柄
是一个真正的数据库文件打开或已附加了一个或多个真实的数据库文件使用attach机制,或者当一个真正的数据库文件从一种超脱数据库连接
使用DETACH语法。同样,如果共享缓存模式
已启用,则会发生异常。在这种情况下,数据库连接
在其用户数达到零之前不会关闭。关闭数据库连接
所需的IO相关步骤在closing_database_connection一节中介绍。
TODO:第4部分和第5部分完成后,回到这里,看看我们是否可以添加与每个数据库连接相关的状态项列表,以便更容易理解。即每个数据库连接都有一个文件句柄,页面缓存中的一组条目,期望的页面大小等。
Opening a New Connection
本节介绍创建新数据库连接时发生的VFS操作。
打开一个新的数据库连接是一个两步过程:
- 文件句柄在数据库文件上打开。
- 如果第1步成功,则尝试使用新文件句柄从数据库文件中读取
数据库文件头
。
在上述过程的第2步中,数据库文件在读取之前未锁定。这是read_data部分中描述的锁定规则的唯一例外。
试图读取数据库文件头的原因
是为了确定数据库
文件使用的页面大小
。因为不在数据库
文件上保留至少一个共享锁
,因此无法确定页面大小
(因为在读取数据库文件头
之后,其他数据库连接
可能已更改),因此从数据库
读取的值文件头
被称为期望的页面大小
。
当需要新的数据库连接
时,SQLite将尝试打开数据库文件上的文件句柄。如果尝试失败,则不会创建新的数据库连接
并返回错误。
当需要新的数据库连接
时,打开新的文件句柄后,SQLite应尝试读取数据库文件的前100个字节。如果由于除打开的文件大小小于100字节之外的其他原因而导致尝试失败,则关闭文件句柄,不会创建新的数据库连接
,而是返回错误。
如果从新打开的数据库文件中成功读取数据库文件头
,则应将期望
的连接页面大小
设置为存储在数据库头的页面大小字段
中的值。
如果数据库文件头
不能从一个新打开的数据库文件中读取(因为文件的大小小于100个字节),连接预期的页大小
应被设置为SQLITE_DEFAULT_PAGESIZE选项的编译时间价值。
Closing a Connection
本节介绍当现有数据库连接关闭(销毁)时发生的VFS操作。
关闭数据库连接是一件简单的事情。打开的VFS文件句柄关闭,并释放与内存页面缓存
相关的资源。
当数据库连接
关闭时,SQLite应关闭VFS级别的关联文件句柄。
当数据库连接
关闭时,所有关联的页面缓存
条目都应该被丢弃。
The Page Cache
SQLite数据库文件的内容被格式化为一组固定大小的页面。有关所用格式的完整说明,请参阅ff_sqlitert_requirements。用于特定数据库的页面大小
作为数据库文件头的一部分存储在文件的前100个字节内众所周知的偏移处。几乎所有由SQLite对数据库文件执行的读写操作都是在大小为数据页大小的
字节块上完成的。
在单个进程中运行的所有SQLite数据库连接共享一个页面缓存
。页面缓存
以每页为基础缓存
从主内存中的数据库文件中读取的数据。当SQLite要求来自数据库文件的数据满足数据库查询时,它会在从数据库文件中加载数据库文件之前,检查 页面缓存中
所需数据库页面的可用缓存版本。如果找不到可用的高速缓存条目并从数据库文件加载数据库页面数据,则它将缓存在页面高速缓存中,
以备稍后再次使用相同的数据。因为从数据库文件中读取数据的速度比从主内存中读取数据要快一个数量级,所以在页面缓存中
缓存数据库页面内容 最大限度地减少对数据库文件执行的读取操作次数,这是一项显着的性能提升。
该页面缓存
也被用来缓冲数据库的写操作。当需要SQLite修改组成数据库文件的多个数据库页面时
,它首先会修改页面缓存中
页面的缓存版本。此时该页面被认为是“脏”页面。在稍后的某个时间点,“脏”页面的新内容通过VFS界面从页面缓存
复制到数据库文件中。在页面缓存中
缓冲写入操作可减少数据库文件所需的写入操作次数(在同一页面更新两次的情况下),并允许基于fs_performance部分中概述的假设进行优化。
数据库读写操作以及它们与页面缓存
交互和使用的方式分别在本文档的reading_data和writing_data部分进行了详细介绍。
在任何时候,页面缓存都
包含零个或多个页面缓存条目
,每个缓存条目
都有以下与其关联的数据:
对相关
数据库连接的
引用。页面缓存
中的每个条目都与单个数据库连接
相关联; 创建条目的数据库连接
。一个页面缓存条目
只会被使用的数据库连接
创建它。页面缓存条目
不在数据库连接
之间共享。
- 该
页码
缓存的网页。页面在数据库文件中从第1页开始按顺序编号(第1页从字节偏移量0开始)。有关详细信息,请参阅ff_sqlitert_requirements。
- 该
缓存数据
;大小
为数据页大小
字节的块。
上面列表中的前两个元素(关联的数据库连接
和页码
)唯一标识页面缓存条目
。任何时候页面缓存
都不能包含两个数据库连接
和页码
都相同的条目。换句话说,单个数据库连接
永远不会在页面缓存中
缓存多个数据库页面的副本。
在任何时候,根据以下定义,每个页面缓存条目
可以被认为是干净页面
,不可写脏页面
或可写脏页面
:
- 一个
干净的页面
是为其缓存的数据当前数据库文件的相应页面中的内容相匹配。该页面从文件加载后未被修改。
- 一个
脏页
是一个网页缓存条目
为此,因为它是从数据库文件加载,所以不再相应的数据库文件页面的当前内容相匹配的高速缓存的数据已被修改。一个脏页
是一个当前缓冲到数据库文件制作成的部分修改写事务
。
- 在本文档中,术语
不可写脏页面
特别用于指代具有修改内容的页面缓存条目
,对于该条目,
用数据库文件更新尚不安全。根据fs_assumption_details中的假设,如果在更新过程中或更新后发生的电源或系统故障可能导致数据库在系统恢复后变得损坏,那么使用缓冲写入来更新数据库文件是不安全的。
- 一个
脏页面
可以安全地用修改后的内容更新相应的数据库文件页面而不会冒数据库损坏的风险,称为可写的脏页面
。
用于确定具有修改的内容的页面缓存条目
是否为脏页面
或可写页面
的确切逻辑在page_cache_algorithms部分中介绍。
由于主内存是有限的资源,因此不能允许页面缓存
无限增长。因此,除非进程内的数据库连接打开的所有数据库文件都非常小,否则有时必须从页面缓存中
丢弃数据。在实践中,这意味着页面缓存条目
必须清除,以便为新的空间腾出空间。如果一个页面缓存条目
正在从删除页面缓存
以释放主存储是一个肮脏的页面
,然后将其内容必须被保存到数据库文件之前,它可以在不丢失数据被丢弃。以下两个小节描述了页面缓存
使用的算法来确定何时存在页面缓存条目
被清除(丢弃)。
Page Cache Configuration
TODO:描述设置为配置页面缓存限制的参数。
Page Cache Algorithms
TODO:描述使用配置参数的方式的要求。关于LRU等
Reading Data
为了将数据从数据库返回给用户,例如作为SELECT查询的结果,SQLite必须在某个时刻从数据库文件中读取数据。通常,数据是以页面大小
字节的对齐块从数据库文件中读取的。在数据库使用的页面大小
可以被知道之前,正在检查数据库文件头部字段是个例外。
除了两个例外,数据库连接
在从数据库文件读取数据之前,必须在数据库上
有一个打开的事务(无论是只读事务
还是读/写事务
)。
两个例外是:
- 尝试在打开
数据库连接
后立即读取100字节的数据库文件头时
(请参阅open_new_connection一节)。发生这种情况时,数据库文件上没有锁定。
- 在打开只读事务的过程中读取数据(请参见open_read_only_trans部分)。这些读取操作发生在数据库文件上的
共享锁定
之后。
一旦事务被打开,从数据库连接读取数据是一个简单的操作。使用数据库文件上打开的文件句柄的xRead()方法,一次只读取一个所需的数据库文件页。SQLite永远不会读取部分页面,并且总是对每个需要的页面使用对xRead()的单个调用。
在读取数据库页面的数据之后,SQLite将原始页面的数据存储在页面缓存中
。每次上层需要一页数据时,查询页面缓存
以查看它是否包含当前数据库连接
存储的所需页面的副本。如果可以找到这样的条目,则从页面缓存
而不是数据库文件
中读取所需的数据。只有与数据库上的开放事务事务(只读事务
或读/写事务
)的连接才可以从页面高速缓存
读取数据。从这种意义上说,从页面缓存中
读取与从中读取没有什么不同数据库文件
。
有关页面数据在页面缓存中的
存储方式和时间的说明,请参阅page_cache_algorithms一节。
除了H35070所要求的读取操作以及作为打开只读事务的一部分的读取操作之外,SQLite应确保数据库连接
在从数据库文件
读取任何数据时具有开放的只读或读/写事务。
除了H35070和H21XXX描述的那些读取操作外,SQLite应以页大小
字节的对齐块的形式从数据库文件读取数据,其中page-size
是数据库文件使用的数据库页大小
。
在使用存储在页面缓存
中的数据
来满足用户查询之前,SQLite应确保数据库连接
具有开放的只读或读/写事务。
Opening a Read-Only Transaction
在从数据库文件中
读取数据或从页面缓存中
查询数据之前,必须通过关联的数据库连接成功打开只读事务
(即使连接最终将写入数据库也是如此,例如读/写事务
只能通过从只读事务
升级来打开)。本节介绍打开只读事务的过程
。
只读事务
的关键元素是在数据库文件上打开的文件句柄获取并保存数据库文件上的共享锁
。因为连接在实际上可能修改数据库文件的内容之前需要排
它锁
,并且根据定义,当一个连接持有共享锁时,
其他连接不会持有排他锁
,持有共享锁可
保证不其他进程可能会在只读事务
保持打开状态时修改数据库文件。这确保了只读事务
与其他数据库用户的交易充分隔离(请参阅部分概述)。
在数据库文件上获取共享锁
本身非常简单,SQLite只是调用数据库文件句柄的xLock()方法。作为打开只读事务的
一部分而发生的其他一些过程非常复杂。SQLite需要按照它们必须发生的顺序打开一个只读事务的
步骤如下:
- 甲
共享锁
数据库文件被获得。
- 连接检查文件系统中是否存在
热日志文件
。如果有,那么它会在继续之前回滚。
- 连接检查
页面缓存中
的数据是否仍然可信。如果不是,则会丢弃所有页面缓存数据。
- 如果文件大小不是零字节且页面缓存不包含数据库第一页的有效数据,则必须从数据库读取第一页的数据。
当然,尝试上面列举的4个步骤中的任何一个时都可能发生错误。如果发生这种情况,则释放共享锁
(如果已获得),并向用户返回错误。以上过程的第2步在hot_journal_detection一节中有更详细的描述。Section cache_validation描述了上面第3步所标识的进程。有关步骤4的更多详细信息,请参见read_page_one部分。
当需要使用数据库连接
打开只读事务时
,SQLite应首先尝试获取数据库文件上打开的文件句柄上的共享锁
。
如果在打开只读事务时
,SQLite无法获得数据库文件上的共享锁定
,则放弃该进程,不打开事务,并向用户返回错误。
尝试获取共享锁
可能失败的最常见原因是某些其他连接持有排他锁
或挂起锁
。但是它也可能失败,因为在对xLock()方法的调用中发生了一些其他错误(例如,IO或通信相关的错误)。
在成功获取数据库文件的共享锁
之后,打开只读事务时
,SQLite应尝试检测并回滚与同一数据库文件关联的热日志
文件。
如果在打开只读事务
时SQLite在尝试检测或回滚热日志文件时
遇到错误,那么数据库文件上的共享锁
被释放,没有事务打开并且错误返回给用户。
部分hot_journal_detection包含上述要求中涉及检测热日志文件的说明和要求。
假设没有发生错误,那么在尝试检测并回滚热日志文件后
,如果页面缓存
包含与当前数据库连接
相关的任何条目,则SQLite应通过测试文件更改计数器来
验证页面缓存
的内容。这个过程被称为缓存验证
。
该缓存验证
过程被详细描述在节cache_validation描述
如果需要通过H35040规定的缓存验证程序,并不能证明该页面缓存
使用当前的相关条目的数据库连接
是有效的,那么SQLite的应放弃与当前相关联的所有条目的数据库连接
从页面缓存
。
上面的编号列表指出,如果数据库文件的第一页存在并且尚未加载到页面缓存中
,则必须从数据库文件中读取
数据库文件的第一页的数据,然后才能将只读事务
视为打开。这由H35240要求处理。
Hot Journal Detection
本节介绍SQLite用来检测热日志文件的过程
。如果检测到热日志文件
,则表示在某个时刻,向数据库写入事务的过程中断,并且需要执行恢复操作(热日志回滚
)。本部分没有描述热日志回滚
的过程(请参阅hot_journal_rollback一节)或者可能会创建热日志文件的过程
(请参阅writing_data一节)。
用于检测热日志文件的
过程非常复杂。以下步骤发生:
- 使用VFS xAccess()方法,SQLite查询文件系统以查看与数据库关联的日志文件是否存在。如果没有,则没有热日志文件。
- 通过调用在数据库文件上打开的文件句柄的xCheckReservedLock()方法,SQLite会检查其他连接是否持有
保留锁
或更高版本。如果其他连接不包含保留锁
,则表明另一个连接处于读/写事务的
中途(请参见writing_data部分)。在这种情况下,日志文件
不是热日志
,不能回滚。
- 使用数据库文件上打开的文件句柄的xFileSize()方法,SQLite检查数据库文件的大小是否为0字节。如果是,则日记文件不被视为
热日志
文件。通过调用VFS xDelete()方法,它不是回滚日志文件,而是从文件系统中删除它。TODO:从技术上讲,这里存在竞争条件。这个步骤应该在独占锁定后进行。
- 尝试升级到数据库文件的
排他锁
。如果尝试失败,则包括最近获取的共享锁
在内的所有锁都将被丢弃。尝试打开只读事务
失败。当某些其他连接也尝试打开只读事务
并且尝试获得排它锁
失败时,会发生这种情况,因为其他连接也持有共享锁
。留给另一个连接来回滚热日志
。将文件句柄锁直接从共享
升级为独占非常重要
在这种情况下,而不是首先升级到保留
锁或挂起
锁,这是在获得独占锁
以写入数据库文件(section writing_data)时所需的。如果SQLite 在这种情况下首先升级到保留
锁或挂起
锁,那么第二个进程也试图打开数据库文件的读事务
可能会在此过程的第2步中检测到保留
锁,从而得出结论:没有热日记
,并开始从数据库文件中
读取数据。
- 再次调用xAccess()方法来检测日志文件是否仍在文件系统中。如果是,则它是一个热日志文件,SQLite会尝试将其回滚(请参阅回滚部分)。
TODO:主日志文件指针?
以下要求更详细地描述了上述过程的第1步。
当需要尝试检测热日志文件时
,SQLite应首先使用VFS层的xAccess()
如果H35140所需的对xAccess()的调用失败(由于IO错误或类似原因),那么SQLite将放弃尝试打开只读事务
,放弃数据库文件中保存的共享锁
并向其返回错误用户。
当需要尝试检测热日志文件时
,如果H35140所要求的对xAccess()的调用表明日志文件不存在,则SQLite应该断定文件系统中没有热日志文件
,因此不需要热日志回滚
。
以下要求更详细地描述上述步骤的第2步。
当需要尝试检测热日志文件时
,如果H35140所需的对xAccess()的调用指示存在日志文件,则调用数据库文件file-handle的xCheckReservedLock()方法以确定是否一些其他进程正在对数据库文件保留
一个保留
或更大的锁定。
如果H35160所需的对xCheckReservedLock()的调用失败(由于IO或其他内部VFS错误),则SQLite应放弃尝试打开只读事务
,放弃数据库文件中保存的共享锁
并返回错误给用户。
如果H35160所需的对xCheckReservedLock()的调用表明某个其他数据库连接
正在数据库文件上保留
一个保留
或更大的锁,那么SQLite应该断定没有热日志文件
。在这种情况下,结束检测热日志文件
的尝试。
以下要求更详细地描述了上述步骤的第3步。
如果在尝试检测热日志文件时
调用xCheckReservedLock()表示没有进程在数据库文件
上保留
一个保留的
或更大的锁,那么SQLite应该使用VFS xOpen()在潜在热日志文件上打开一个文件句柄的方法。
如果H35440所需的对xOpen()的调用失败(由于IO或其他内部VFS错误),则SQLite应放弃尝试打开只读事务
,放弃数据库文件中保存的共享锁
并返回错误给用户。
在成功打开潜在热日志文件上的文件句柄后,SQLite应使用打开文件句柄的xFileSize()方法以字节为单位查询文件的大小。
如果H35450所需的对xFileSize()的调用失败(由于IO或其他内部VFS错误),则SQLite应放弃尝试打开只读事务
,放弃数据库文件中保存的共享锁
,关闭文件句柄在日志文件上打开并将错误返回给用户。
如果通过H35450所需查询发现潜在热日志文件
的大小为零字节,则SQLite应关闭在日志文件上打开的文件句柄,并使用对VFS xDelete()方法的调用来删除日志文件。在这种情况下,SQLite应该得出结论:没有热日志文件
。
如果H35450所需的对xDelete()的调用失败(由于IO或其他内部VFS错误),则SQLite应放弃尝试打开只读事务
,放弃数据库文件中保存的共享锁
并返回错误给用户。
以下要求更详细地描述了上述过程的第4步。
如果通过H35450所需的查询发现潜在热日志文件的大小大于零字节,则SQLite应尝试将数据库文件
上的数据库连接
所持有的共享锁
直接升级为独占锁
。
如果尝试升级到由H35470规定的独占锁
由于任何原因失败,则SQLite应释放数据库连接
保留的所有锁并关闭在日志文件
上打开的文件句柄。试图打开一个只读事务
应被视为失败,并向用户返回一个错误。
最后,以下要求更详细地描述了上述过程的第5步。
如果作为热日志文件
检测过程的一部分,尝试升级到由H35470强制执行的独占锁定
是成功的,则SQLite将使用VFS实现的xAccess()方法来查询文件系统,以测试日志文件仍然存在于文件系统中。
如果H35490所需的对xAccess()的调用失败(由于IO或其他内部VFS错误),那么SQLite将放弃尝试打开只读事务
,放弃数据库文件上的锁定,关闭文件句柄在日志文件上打开并向用户返回错误。
如果H35490所需的对xAccess()的调用显示日志文件不再存在于文件系统中,则SQLite应放弃尝试打开只读事务
,放弃数据库文件上的锁定,关闭文件句柄在日志文件上打开并返回一个SQLITE_BUSY错误给用户。
如果H35490所需的xAccess()查询显示日志文件在文件系统中仍然存在,则SQLite应该断定该日志文件是需要回退的热日志文件
。SQLite应立即开始热日志回滚
。
Cache Validation
当数据库连接
打开读取事务时
,页面缓存
可能已经包含与数据库连接
关联的数据
。但是,如果另一个进程在加载缓存页面后修改了数据
库文件,则可能缓存的数据
无效。
SQLite 使用文件更改计数器
(数据库文件头
中的一个字段)确定属于数据库连接
的页面缓存
条目是否有效。所述文件的改变计数器
是存储在起始字节4字节大端整数字段偏移的24 数据库文件头
。在以任何方式修改数据库文件内容的读/写事务
结束之前(请参见writing_data部分),存储在文件更改计数器中
的值将递增。当数据库连接
解锁数据库文件时,它会存储数据库的当前值文件更改计数器
。之后,在打开新的只读事务时
,SQLite会检查数据库文件中存储的文件更改计数器
的值。如果自从数据库文件解锁后该值未更改,则可以信任页面缓存
条目。如果值已更改,则页缓存
条目不可信,并且与当前数据库连接
关联的所有条目均被丢弃。
当数据库文件上打开的文件句柄被解锁时,如果页面缓存
包含一个或多个属于关联数据库连接的
条目,则SQLite应在内部存储文件更改计数器
的值。
当需要执行高速缓存验证
作为打开读事务的
一部分时,SQLite应使用数据库连接
文件句柄的xRead()方法从数据库文件的
字节偏移量24开始读取一个16字节块。
TODO:为什么是16字节块?为什么不是4?(与加密数据库有关)。
在执行高速缓存验证时
,在加载H35190所要求的16字节块之后,SQLite应将块的前4个字节中存储的32位大端整数与文件更改计数器的
最近存储值进行比较(请参阅H35180 )。如果这些值不相同,那么SQLite应该得出结论,缓存的内容是无效的。
要求H35050(open_read_only_trans部分)指定SQLite在确定缓存内容无效时需要执行的操作。
Page 1 and the Expected Page Size
作为在大于0字节的数据库文件上打开读取事务
的最后一步,SQLite需要将数据库页面1的数据加载到页面缓存中
(如果它尚未存在)。这比看起来稍微复杂一点,因为数据库页面大小
在这一点上是未知的。
即使数据库页面大小
无法确定,但SQLite通常可以通过假设它等于预期页面
连接大小
来正确猜测。该预期的页面大小
是价值页面大小
字段中,从读取数据库文件头
,同时打开数据库连接(见open_new_connection)或页面大小
时,大部分的数据库文件的读取交易
得出的结论。
在读取事务
结束期间,在解锁数据库文件之前,SQLite应将连接期望页面大小
设置为当前数据库页面大小
。
作为打开新读取事务的一部分
,在执行高速缓存验证
后立即执行,如果页面高速缓存中
没有数据
库页面1的数据
,则SQLite应使用连接的xRead()方法从数据
库文件的起始处读取N
个字节文件句柄,其中N
是连接当前期望的页面大小
值。
如果按照H35230的要求读取第1页数据,则数据库文件头中出现的页面大小
字段的值将消耗读取块的前100个字节,而与当前期望页面
的连接大小不同
,则在预期的页面大小
设置为这个值,数据库文件被解锁并打开整个程序读取事务
被重复。
如果按照H35230的要求读取第1页数据,则数据库文件头中出现的消耗读取块前100个字节的页面大小
字段的值与连接当前期望的页面大小相同
,读取的数据块作为页面1 存储在页面缓存
中。
Reading Database Data
TODO:添加一些关于首先检查页面缓存等的内容
Ending a Read-only Transaction
要结束只读事务
,SQLite只需放弃数据库文件上打开的文件句柄上的共享锁
。不需要其他操作。
当需要结束只读事务时
,SQLite应通过调用文件句柄的xUnlock()方法来放弃数据库文件中的共享锁
。
Writing Data
使用DDL或DML SQL语句,SQLite用户可以修改数据库文件的内容和大小。ff_sqlitert_requirements中描述了如何将逻辑数据库的更改转换为对数据库文件的修改。从本文档中描述的子系统的角度来看,执行的每个DDL或DML语句都会导致零个或多个数据库文件页面的内容被新数据覆盖。DDL或DML语句也可能追加或截断数据库文件末尾的一个或多个页面。一个或多个DDL和/或DML语句组合在一起构成单个写入事务
。甲写入事务
需要具有在节概述中描述的特殊性能; 一个写事务
必须是孤立的,持久的和原子的。
SQLite使用以下技术来实现这些目标:
- 为确保
写入事务
处于隔离状态
,在开始修改数据库文件
的内容以反映写入事务
的结果之前,SQLite会在数据库文件
上获得排它锁
。在写入交易
结束之前,锁不会放弃。由于从数据库文件
读取需要共享锁
(请参阅reading_data一节),并且保留排它锁可
确保没有其他数据库连接
正在持有或可以获取共享锁
,这可确保没有其他连接可以从数据库文件
在写事务
部分应用的时候。
- 确保
写入交易
是原子
是系统所需的最复杂的任务。在这种情况下,原子
意味着即使发生系统故障,尝试向数据库文件提交写入事务
也会导致作为事务的一部分的所有更改成功应用于数据库文件,或者没有任何更改已成功应用。没有机会仅应用一部分更改。因此,从外部观察者的角度来看,写入事务
似乎是一个原子
事件。
当然,通常不可能以原子方式将写入事务所
需的所有更改应用于文件系统内的数据库文件。例如,如果写入事务
需要修改10个数据库文件页面,并且在sqlite仅修改了5个页面后断电导致系统发生故障,那么在系统恢复之后,数据库文件几乎肯定会处于不一致的状态。
SQLite通过使用日志文件
解决了这个问题。在几乎所有情况下,在以任何方式修改数据库文件
之前,SQLite会在日志文件中
存储足够的信息,以便在数据库文件
正在更新以反映所做的修改时发生系统故障时允许重建原始数据库文件
通过写入交易
。每次SQLite打开一个数据库文件
时,它都会检查是否发生了这样的系统故障,如果是,则根据日志文件
的内容重新构建数据库文件
。hot_journal_detection部分描述了用于检测此流程是否需要创建热日志回滚的
过程。热日志回滚
本身在hot_journal_rollback部分中进行了介绍。
同样的技术可以确保SQLite数据库文件不会因不合时宜的系统故障而损坏。如果在SQLite有机会执行足够的同步文件
操作以确保组成写入事务
的更改使其安全地存储到永久存储器之前发生系统故障,则会使用日志文件
将数据库还原到在系统恢复后已知的良好状态。
- 这样
写事务
是持久的
系统故障的面孔,SQLite的执行同步文件
在结束之前,对数据库文件
的操作写入事务
的页面缓存
用于缓冲的修改数据库文件
的图像,他们被写入之前,数据库文件
。当页面内容需要修改为写入事务
中的操作结果时,修改的副本将存储在页面缓存中
。同样,如果新页面附加到数据库文件
的末尾,则会将它们添加到页面缓存中
而不是立即写入文件
系统内的数据库文件
。理想情况下,整个写入事务
的所有更改都会缓存在页面缓存中
,直到事务结束。当用户提交事务时,所有更改都将以最有效的方式应用于数据库文件
,同时考虑到fs_performance部分列举的假设。不幸的是,由于主内存是有限的资源,对于大型事务来说,这并不总是可能的。在这种情况下,更改将缓存在页面缓存中
直到达到一些内部条件或限制,然后写入数据库文件
,以便根据需要释放资源。page_cache_algorithms部分描述了将更改刷新到数据库文件
中间事务以释放页面缓存
资源的情况。即使在写入事务
正在进行时未发生应用程序或系统故障,还原数据库文件
和页面缓存
的回滚操作到交易开始之前的状态。如果用户显式请求事务回滚(通过发出“ROLLBACK”命令),或者由于遇到SQL约束(请参阅sql_sqlitert_requirements)而自动发生,则可能会发生这种情况。因此,在页面缓存
内甚至修改页面之前,原始页面内容都存储在日志文件中
。TODO:介绍以下小节。日志文件
格式本节介绍SQLite日志文件
使用的格式。日志文件
由一个或多个日志标题,
零个或多个日志记录
以及可选的主日志指针组成
。每个日志文件
始终以日志标题
开头,随后是零个或多个日志记录
。接下来可能是第二个日志标题,
然后是第二组零个或更多的日志记录
等等。日志文件
可能包含的日志标题
的数量没有限制。在日志标题
及其伴随的日志记录
集之后可以是可选的主日志指针
。或者,该文件可能会在最终的期刊记录之后
结束。本部分仅描述日志文件
的格式以及组成它的各种对象。但是因为日志文件
可能在系统故障恢复后被SQLite进程读取(热日志回滚
,请参见hot_journal_rollback一节),因此使用以下组合来描述文件的创建和填充方式也很重要:写入文件
,同步文件
和截断文件
操作。这些在write_transactions部分中描述。日志标题
格式日志标题的大小
是扇区大小
,其中扇区大小
是由在数据库文件
上打开的文件句柄的xSectorSize方法返回的值。只有日志标题
的前28个字节被使用,其余的可能包含垃圾数据。每个日志头
的前28个字节由一个8字节的块设置为一个众所周知的值,然后是5个大端的32位无符号整数字段。
- 在
写事务
被打开。这个过程在opens_a_write_transaction部分中描述。
- 最终用户执行需要修改数据库文件的数据库文件结构的DML或DDL SQL语句。这些修改可能是操作的任何组合
- modify the content of an existing database page,
- append a new database page to the database file image, or
- truncate (discard) a database page from the end of the database file.
这些操作在modify_appending_truncating一节中有详细介绍。在ff_sqlitert_requirements中描述了用户DDL或DML SQL语句如何映射到这三个操作的组合。
- 在
写事务
的结论和所做的更改永久提交到数据库。提交事务所需的过程在committing_a_transaction部分中进行了描述。作为上述步骤3的替代方案,交易可能会回滚。事务回滚在部分回滚中描述。最后,还要记住在任何时候写入事务都
可能因系统故障
而中断。在这种情况下,文件系统(数据库文件
和日志文件
)的内容必须保持在这种状态,以便使数据库文件
能够恢复到中断的写入事务
之前的状态开始了。这被称为热日志回滚
,在hot_journal_rollback一节中进行了介绍。第fs_assumption_details节描述了在恢复之后关于系统故障
对文件系统内容的影响的假设。开始写入事务
在任何数据库页面可能在页面缓存
内被修改之前,数据库连接
必须打开一个写入事务
。打开写入事务
要求数据库连接
在数据库文件
上获得一个保留锁
(或更高)。由于获得保留锁
在数据库文件
保证没有其他数据库连接
可以持有或获得保留锁
或更高版本,因此,没有其他数据库连接
可能具有打开的写入事务
。一个保留锁
的数据库文件
可以被认为是对的排他锁的日志文件
。没有数据库连接
可以读取或写入日志文件,
而不在相应的数据库文件
上保留
或更大的锁定。在打开一个写事务
之前,一个数据库连接
必须有一个开放的读取事务
,通过open_read_only_trans部分中描述的过程打开。这确保没有需要回滚的热日志文件
,并且可以信任存储在页面缓存
中的任何数据。一旦读取事务
被打开,升级到写入事务
是一个两步过程,如下所示:
- 甲
保留锁
上获得的数据库文件
。
- 该
日志文件
被打开,并在必要时(使用VFS XOPEN方法)创建和日志文件头
写入的用它来对文件中的单个呼叫开始处理xWrite方法。
详细描述上述步骤第1步的要求:
当需要在数据库上打开一个写事务
时,如果有问题的数据库连接
还没有打开,那么SQLite应该首先打开一个读事务
。
当需要在数据库上打开一个写事务
时,在确保读事务
已经打开后,SQLite应该通过调用数据库文件上打开的文件句柄的xLock方法来获得数据库文件的保留锁
。
如果试图获得由要求H35360规定的保留锁定
失败,则SQLite将认为尝试打开写入事务
失败并向用户返回错误。
详细描述上述步骤第2步的要求:
当需要在数据库上打开一个写事务
时,在获得数据库文件的保留锁
后,SQLite应该在相应的日志文件
上打开一个读/写文件句柄。
当需要在数据库上打开一个写入事务
时,在打开日志文件
上的文件
句柄之后,SQLite应该向(当前为空的)日志文件
添加日志标题
。
Writing a Journal Header
描述如何将日志标题
附加到日志文件的要求:
当需要将日志头
添加到日志文件时
,SQLite应该通过使用对日志文件
上打开的文件句柄的xWrite方法的单次调用来写入一个扇区大小的
字节块来实现。写入的数据块应以日志文件
当前末端或后面的最小扇区大小对齐偏移量开始。
需要由H35680写入的日志头
的前8个字节按字节偏移0到7的顺序包含以下值:0xd9,0xd5,0x05,0xf9,0x20,0xa1,0x63和0xd7。
需要由H35680写入的日志头的
字节8-11 应包含0x00。
需要由H35680写入的日志头的
字节12-15 应包含当前写入事务
开始时数据库文件包含的页面数,格式为4字节的大端无符号整数。
需要由H35680写入的日志头的
字节16-19 应包含伪随机生成的值。
需要由H35680写入的日志头的
字节20-23 应包含VFS层使用的扇区大小
,格式化为4字节的大端无符号整数。
需要由H35680写入的日志头的
字节24-27 应包含数据库在写入事务
开始时使用的页面大小
,格式为4字节的大端无符号整数。
Modifying, Adding or Truncating a Database Page
当最终用户执行DML或DDL SQL语句来修改数据库模式或内容时,SQLite需要更新数据库文件映像以反映新的数据库状态。这涉及到修改多个数据库文件页面中的一个或多个数据库文件页面的内容,附加或截断它 不是直接使用VFS接口修改数据库文件,而是首先在页面缓存中缓存
更改。
在修改页面缓存
中可能需要通过回滚操作恢复的数据库页面之前,必须对该页面进行日志记录
。挂起页面
是将页面原始数据复制到日志文件中以便在写入事务
回滚时可以恢复的过程。journalling一个页面的过程在journalling_a_page一节中描述。
当需要修改已经存在,并没有一个现有的数据库页面的内容自由列表叶页
的时候写事务
被打开了,SQLite的应杂志,如果它尚未在当前轴颈内页写事务
。
当需要修改现有数据库页面
的内容时,SQLite应更新作为与页面
关联的页面缓存条目的
一部分存储的数据库页面
内容的缓存版本。
当一个新的数据库页被附加到一个数据库文件时,不需要将记录添加到日志文件中
。如果需要回滚,则数据库文件将根据存储在日志文件的
字节偏移量12处的值简单地截断回原始大小。
当需要将新的数据库页面
附加到数据库文件时,SQLite应创建一个与新页面
对应的新页面缓存条目
并将其插入到页面缓存中
。新页面缓存条目
的脏标志
应被设置。
如果需要从数据库文件的末尾截断数据库页面,则关联的页面缓存条目将
被丢弃。数据库文件的调整大小存储在内部。在提交当前写入事务
之前,数据库文件实际上并未被截断(请参见committing_a_transaction部分)。
当从数据库文件的末尾打开写入事务
时,如果需要截断(移除)存在且不是自由列表叶页
的数据库页面,则SQLite应记录该页面,如果它尚未被当前记录在当前写交易
。
当需要从数据库文件的末尾截断数据库页面时,SQLite应该从页面缓存中放弃关联的页面缓存项
。
Journalling a Database Page
通过将日志记录
添加到日志文件
来对页面进行 日记
。日记记录
的格式在journal_record_format部分中进行了介绍。
当需要记录数据库页面时
,SQLite应首先将正在记录的页面的页码
附加到日志文件中
,并将其格式化为4字节的大端无符号整数,并对文件句柄的xWrite方法进行一次调用在日志文件上打开。
当需要记录数据库页面时
,如果尝试将页码
添加到日志文件中是成功的,则应当使用对xWrite方法的单个调用将当前页面数据(页面大小的
字节)附加到日志文件中在日志文件上打开的文件句柄。
当需要记录数据库页面时
,如果尝试将当前页面数据附加到日志文件成功,则SQLite应该使用一次调用将一个4字节的big-endian整数校验和值附加到日志文件中日志文件上打开的文件句柄的xWrite方法。
在页面数据(要求H35290)之后立即写入日志文件
的校验和值是存储在日志标题中
的页面数据和校验和初始值设定项
字段的函数
(请参见journal_header_format部分)。具体来说,它是校验和初始值设定项
和页数据的第200个字节的值的总和,被解释为8位无符号整数,从页数据的第(page-size
%200)个字节开始。例如,如果页面大小
为1024字节,则通过将偏移量为23,223,423,623,823和1023(页面的最后一个字节)的字节值与该校验初始化
.
通过H35290所要求的写入写入日志文件
的校验和值应该等于存储在日志头部
(H35700)中的校验和初始值设定
字段与页面数据的每第200个字节之和,从(页面大小
% 200)字节。
在要求H35300中使用'%'字符来表示模运算符,就像在C,Java和Javascript等编程语言中一样。
Syncing the Journal File
即使通过调用日志文件file-handle xWrite方法(部分journalling_a_page)将数据库页面的原始数据写入日志文件中,但在数据库文件内写入页面仍然不安全。这是因为如果发生系统故障,写入日志文件的数据可能仍然被破坏(请参阅fs_characteristics一节)。在页面可以在数据库本身内更新之前,会发生以下过程:
- 调用在日志文件上打开的文件句柄的xSync方法。此操作可确保日志文件中的所有
日志记录
已写入持久性存储,并且不会由于后续系统故障而损坏。
- 日志文件中最近写入的日志标题的
日志记录计数
字段(请参见部分journal_header_format)将更新为包含自写入标题后添加到日志文件的日志记录
数。
- 再次调用xSync方法,以确保对
日记记录计数
的更新已提交到持久存储。
如果以上枚举的所有三个步骤都成功执行,那么修改数据库文件本身内的带日记的
数据库页面的内容是安全的。上述三个步骤的组合称为同步日志文件
。
当需要同步日志文件时
,SQLite应调用日志文件
上打开的文件句柄的xSync方法。
当需要同步日志文件时
,在按照H35750的要求调用xSync方法后,SQLite应更新最近写入日志文件
的日志标题
的记录计数
。4字节字段应更新为包含自写入日志头
以来已写入日志文件
的日志记录
数,格式为4字节的大端无符号整数。
当需要同步日志文件时
,在按照H35760的要求更新日志头
的记录计数
字段之后,SQLite应调用日志文件
上打开的文件句柄的xSync方法。
Upgrading to an Exclusive Lock
在页面缓存
内修改的页面内容
可能写入数据库文件之前,必须在数据库文件上保留排它锁
。此锁的用途是防止在第一个连接正在写入数据库的过程中,另一个连接从数据库文件中读取数据。无论写入数据库文件的原因是因为事务正在提交,还是释放页面缓存中的
空间,在同步日志文件
之后,总是立即升级到排它锁
。
当需要将升级到独占锁
作为写入事务的一部分时,SQLite应首先尝试获取数据库文件
上的挂起锁
,如果尚未通过调用在数据库文件
上打开的文件句柄的xLock方法来挂起。
当需要升级到独占锁
作为写事务的一部分时,在成功获取挂起的锁之后,
SQLite应尝试通过调用在数据库文件
上打开的文件句柄的xLock方法来获得排它锁
。
TODO:如果无法获得排他锁,会发生什么情况?尝试从保留锁定到挂起锁定失败是不可能的。
Committing a Transaction
提交写入事务
是更新数据库文件的最后一步。提交交易是一个七步过程,总结如下:
- 数据库文件头
更改计数器
字段递增。ff_sqlitert_requirements中描述的更改计数器
由cache_validation部分中描述的高速缓存验证
过程使用。
- 该
日志文件
的同步。同步日志文件
所需的步骤在syncing_journal_file部分中进行了介绍。
- 升级为
排它锁
对数据库文件,如果一个排它锁
尚未持有。Upgrade_to_exclusive_lock 部分描述了升级到独占锁
。
- 将存储在
页面缓存中
的所有脏页面
的内容复制到数据库文件中。将一组脏页面
以页码顺序写入数据库文件以提高性能(有关详细信息,请参阅fs_performance部分中的假设)。
- 同步数据库文件以确保所有更新都安全地存储在永久介质上。
日志文件
上打开的文件句柄关闭,日志文件
本身被删除。此时写交易
交易已经不可撤销地承诺。
- 数据库文件被解锁。
TODO:展开并解释上述一点。
以下要求更详细地描述了上面列举的步骤。
当需要提交写事务时
,SQLite应修改第1页以增加存储在数据库文件头
的更改计数器
字段中的值。
该改变计数
是存储在字节4字节大端整场抵消的24 数据库文件
。H35800所需的页面1的修改是使用修改_发送_截断部分中描述的过程进行的。如果第1页尚未作为当前写入事务的一部分被记入日志,则递增更改计数器
可能需要记录第1页。在所有情况下,页面1对应的页面缓存条目都会
变成脏页面,
作为增加更改计数器
值的一部分。
当需要提交写入事务时
,在增加更改计数器
字段之后,SQLite应同步日志文件
。
当需要提交写入事务时
,在按照H35810的要求同步日志文件
后,如果数据库文件上的排他锁
尚未被保留,则SQLite应尝试升级到排它锁
。
当需要提交写入事务时
,在按照H35810的要求同步日志文件
并确保在H35830要求的数据库文件
上保留排它锁
之后,SQLite应将存储在页面缓存中
的所有脏页
的内容复制到该数据库文件
使用对数据库连接
文件句柄的xWrite方法的调用。每次对xWrite的调用都应将单个脏页
(页面大小
的数据字节)的内容写入数据库文件
。肮脏的页面应按照页码的
顺序从最低到最高写入。
当需要提交写入事务时
,在将任何脏页
的内容按照H35830的要求复制到数据库文件后,SQLite应通过调用数据库连接
文件句柄的xSync方法来同步数据库文件。
当需要提交写事务时
,在按照H35840的要求同步数据库文件之后,SQLite应关闭在日志文件
上打开的文件句柄,并通过调用VFS xDelete方法从文件系统中删除日志文件
。
当需要提交写事务时
,在按照H35850的要求删除日志文件
后,SQLite应通过调用数据库连接
文件句柄的xUnlock方法来放弃数据库文件中
保存的所有锁。
TODO:提交写入事务
后共享锁是否保留?
Purging a Dirty Page
通常,在用户提交活动写入事务
之前,没有数据实际写入数据库文件。例外情况是,如果单个写入事务
包含要存储在页面缓存
中的太多修改。在这种情况下,存储在页面缓存
中的一些数据库文件修改必须在事务提交之前应用于数据库文件,以便可以从页面缓存
中清除关联的页面缓存条目
以释放内存。到达此条件并且必须清除脏页面时,请参见page_cache_algorithms一节。
在将页面缓存条目
的内容写入数据库文件之前,页面缓存条目
必须满足可写脏页面的条件
,如page_cache_algorithms一节中所定义。如果由page_cache_algorithms节中的算法选择用于清除的脏页,则需要SQLite 同步日志文件
。在同步日志文件
后,与数据库连接
关联的所有脏页都会被分类为可写脏页
。
当需要从页面缓存中
清除不可写的脏页时
,SQLite应该在继续进行H35670所需的写操作之前同步日志文件
。
根据H35640的要求同步日志文件
后,SQLite应在继续进行H35670所需的写入操作之前,在日志文件中
追加新的日志标题
。
章节writing_journal_header中描述了将新日志标题
附加到日志文件。
一旦被清除的脏页面是可写的,它就被简单地写入数据库文件。
当需要清除作为脏页面
的页面缓存条目时
,SQLite应该使用对数据库连接
文件句柄的xWrite方法的单个调用将页面数据写入数据库文件。
多文件事务
报表事务
回滚
热日志回滚
事务回滚
语句回滚
参考
1 | C API Requirements Document. |
---|---|
2 | SQL Requirements Document. |
3 | File Format Requirements Document. |
SQLite is in the Public Domain.