本文档介绍了有关如何在UNIX和Windows上实现WAL模式的底层细节。
单独的文件格式描述提供了有关WAL模式下使用的数据库文件和写头日志文件的结构的详细信息 。但是有意省略了锁定协议的细节和WAL-index格式的细节,因为这些细节留给各个VFS实现酌情决定。本文档填充了UNIX和Windows VFS的那些缺失的详细信息。
为了完整起见,某些高级格式化信息包含在文件格式文档中,而当它与WAL模式处理有关时,此处将复制其他内容。
处于活动状态时,WAL模式数据库的状态由三个单独的文件描述:
主数据库文件的格式如 文件格式文档中所述。主数据库中偏移量18和19的文件格式版本号都必须均为2,以指示数据库处于WAL模式。主数据库可以具有基础文件系统允许的任意名称。尽管“ .db”,“。sqlite”和“ .sqlite3”似乎是流行的选择,但不需要特殊的文件后缀。
预写日志或“ wal”文件是前滚日志,记录已提交但尚未应用于主数据库的事务。有关wal文件格式的详细信息,请参见主文件格式 文档的WAL格式小节。通过在主数据库文件名的末尾附加四个字符“ -wal”来命名wal文件。除8 + 3文件系统外,不允许使用此类名称,在这种情况下,文件后缀将更改为“ .WAL”。但是随着8 + 3文件系统越来越稀有,通常可以忽略这种例外情况。
wal-index文件或“ shm”文件实际上并未用作文件。而是,各个数据库客户端会映射shm文件,并将其用作共享内存以协调对数据库的访问,并用作高速缓存以在wal文件中快速定位帧。shm文件的名称是主数据库文件名,后跟四个字符“ -shm”。或者,对于8 + 3文件系统,shm文件是主数据库文件,后缀更改为“ .SHM”。
shm不包含任何数据库内容,并且崩溃后无需恢复该数据库。因此,连接到静态数据库的第一个客户端通常会截断shm文件(如果存在)。由于不需要在崩溃时保留shm文件的内容,因此永不fsync()将shm文件存储到磁盘。实际上,如果存在一种机制,SQLite可以通过该机制告诉操作系统从不将shm文件持久保存在磁盘上,而是始终将其保存在缓存中,则SQLite将使用该机制来避免与shm文件关联的任何不必要的磁盘I / O。 。但是,在标准posix中不存在这样的机制。
由于shm仅用于协调并发客户端之间的访问,因此,如果 设置了独占锁定模式,则会将shm文件省略,以进行优化。当独家锁定模式被设置时,SQLite使用堆存储器代替存储器映射SHM文件。
在积极使用WAL模式数据库时,以上三个文件通常都存在。除非设置了独占锁定模式,否则将省略Wal-Index文件 。
如果最后一个使用数据库的客户端通过调用sqlite3_close()完全关闭,则将自动运行检查点,以便将所有信息从wal文件传输到主数据库中,并且shm文件和wal文件都将取消链接。因此,当没有任何客户端使用数据库时,通常情况是磁盘上仅存在主数据库文件。但是,如果最后一个客户端在关闭之前未调用sqlite3_close(),或者如果最后一个断开连接的客户端是只读客户端,则不会进行最终的清理操作,并且磁盘上可能仍然存在shm和wal文件即使不使用数据库也是如此。
设置PRAGMAlocking_mode = EXCLUSIVE(排他锁定模式)时,仅允许单个客户端一次打开数据库。由于只有一个客户端可以使用该数据库,因此将省略shm文件。单个客户端使用堆内存中的缓冲区代替内存映射的shm文件。
如果读/写客户端在关闭之前调用 sqlite3_file_control(SQLITE_FCNTL_PERSIST_WAL),则在关闭时仍会运行检查点,但不会删除shm文件和wal文件。这允许后续的只读客户端连接到数据库并读取数据库。
WAL-index或“ shm”文件用于协调多个客户端对数据库的访问,并用作缓存来帮助客户端快速定位wal文件中的帧。
由于shm文件不参与恢复,因此shm文件不需要与机器字节顺序无关。因此,shm文件中的数值以主机的本机字节顺序写入,而不是像使用主数据库文件和wal文件那样转换为特定的跨平台字节顺序。
shm文件由一个或多个哈希表组成,其中每个哈希表的大小为32768字节。唯一的区别是,在第一个哈希表的开头雕刻了一个136字节的标头,因此第一个哈希表的大小仅为32632字节。shm文件的总大小始终是32768的倍数。在大多数情况下,shm文件的总大小恰好是32768字节。如果wal文件变得非常大(超过4079帧),则shm文件仅需要超出单个哈希表即可。由于默认的自动检查点阈值为1000,因此WAL文件很少达到使shm文件增长所需的4079阈值。
shm文件的前136个字节为标头。shm标头具有以下三个主要部分:
字节数 | 描述 |
---|---|
0..47 | WAL索引信息的第一份副本 |
48..95 | WAL索引信息的第二份副本 |
96..135 | 检查点信息和锁 |
除了从WAL标头复制的盐值之外,shm标头的各个字段都是主机本机字节顺序中的无符号整数。盐值是WAL标头中的精确副本,并且采用WAL文件使用的字节顺序。整数的大小可以是8、16、32或64位。shm标头的各个字段的详细细分如下:
字节数 | 名称 | 意义 |
---|---|---|
0..3 | 版本 | WAL索引格式的版本号。始终为3007000。 |
4..7 | 未使用的填充空间。必须为零。 | |
8..11 | 我改变 | 无符号整数计数器,每笔交易都会增加 |
12 | 在里面 | “ isInit”标志。初始化shm文件时为1。 |
13 | bigEndCksum | 如果WAL文件使用大端校验和,则为true。如果WAL使用低位字节校验和,则为0。 |
14..15 | 页面 | 数据库页面大小(以字节为单位);如果页面大小为65536,则为1。 |
16..19 | mxFrame | WAL文件中有效和已提交帧的数量。 |
20..23 | 页数 | 数据库文件的大小(以页为单位)。 |
24..31 | aFrameCksum | WAL文件中最后一帧的校验和。 |
32..39 | 盐 | 从WAL文件头复制的两个盐值。这些值是WAL文件的字节顺序,可能与计算机的本机字节顺序不同。 |
40..47 | 求和 | 此标头的字节0到39上的校验和。 |
48..95 | 此标头的字节0到47的副本。 | |
96..99 | n回填 | 先前的检查点已回填到数据库中的WAL帧数 |
100..119 | 读标记[0..4] | 五个“读标记”。每个读取标记是一个32位无符号整数(4个字节)。 |
120..127 | 预留8个文件锁的未使用空间。 | |
128..132 | nBackfillAttempted | 尝试回填但未成功回填的WAL帧数。 |
132..136 | 保留未使用的空间以进一步扩展。 |
偏移量16处的32位无符号整数(并在偏移量64处重复)是WAL中有效帧的数量。因为WAL帧从1开始编号,所以mxFrame也是WAL中最后一个有效提交帧的索引。提交帧是在帧头的字节4到7中具有非零“数据库大小”值的帧,它指示事务结束。
当mxFrame字段为零时,表明WAL为空,所有内容应直接从数据库文件获取。
当mxFrame等于nBackfill时,表明WAL中的所有内容都已写回到数据库中。在这种情况下,所有内容都可以直接从数据库中读取。此外,如果没有其他连接在WAL_READ_LOCK(N)上保持N> 0的锁,则下一个编写器可以自由重置WAL。
mxFrame值始终大于或等于 nBackfill和nBackfillAttempted。
WAL-index标头中偏移量128处的32位无符号整数称为“ nBackfill”。该字段保存WAL文件中已复制回主数据库的帧数。
nBackfill编号永远不会大于mxFrame。当nBackfill等于mxFrame时,这意味着WAL内容已完全写回到数据库中,并且如果N> 0的WAL_READ_LOCK(N)上没有任何锁,则可以重置WAL。
仅在保持WAL_CKPT_LOCK的情况下才能增加nBackfill。但是,在WAL重置过程中,nBackfill会更改为零,并且在保持WAL_WRITE_LOCK时会发生这种情况。
标头中预留了八个字节的空间,以使用sqlite3_io_methods 对象中的xShmLock()方法支持文件锁定。由于某些VFS(例如Windows)可能使用强制性文件锁来实现锁,因此SQLite永远不会读取或写入这八个字节。
这些是支持的八种锁:
名称 | 抵消 | |
---|---|---|
xShmLock | 文件 | |
WAL_WRITE_LOCK | 0 | 120 |
WAL_CKPT_LOCK | 1个 | 121 |
WAL_RECOVER_LOCK | 2个 | 122 |
WAL_READ_LOCK(0) | 3 | 123 |
WAL_READ_LOCK(1) | 4 | 124 |
WAL_READ_LOCK(2) | 5 | 125 |
WAL_READ_LOCK(3) | 6 | 126 |
WAL_READ_LOCK(4) | 7 | 127 |
TBD:有关标题的更多信息
shm文件中的哈希表旨在快速回答以下问题:
FindFrame(P,M):给定页码P和最大WAL帧索引M,返回不超过M的页面P的最大WAL帧索引,如果没有不超过P的帧,则返回NULL M.
令数据类型“ u8”,“ u16”和“ u32”分别表示长度为8位,16位和32位的无符号整数。然后,将shm文件的第一个32768字节单元组织如下:
u8 aWalIndexHeader [136]; u32 aPgno [4062]; u16 aHash [8192];
shm文件的第二个以及所有后续的32768字节单位如下所示:
u32 aPgno [4096]; u16 aHash [8192];
aPgno条目共同记录存储在WAL文件的所有帧中的数据库页号。第一个哈希表上的aPgno [0]条目记录存储在WAL文件的第一帧中的数据库页号。第一个哈希表中的aPgno [i]条目是WAL文件中第i帧的数据库页号。第二个哈希表的aPgno [k]条目是WAL文件中第(k + 4062)帧的数据库页号。shm文件中第n个32768字节哈希表的aPgno [k]条目(对于n> 1)保存存储在第(k + 4062 + 4096 *(n-2))帧中的数据库页号WAL文件。
这是描述aPgno值的略有不同的方法:如果您将所有aPgno值都视为连续数组,则存储在WAL文件的第i帧中的数据库页号将存储在aPgno [i]中。当然,aPgno不是连续数组。前4062个条目位于shm文件的前32768字节单元中,后续值位于shm文件的稍后单元中的4096个条目块中。
一种计算FindFrame(P,M)的方法是从第M个条目开始扫描aPgno数组,并向后开始搜索,并在aPgno [J] == P处返回J。这样的算法将起作用,并且比在整个WAL文件中搜索页码为P的最新帧要快。但是,仍然可以通过使用aHash结构来使搜索更快。
使用以下哈希函数将数据库页号P映射到哈希值:
h =(P * 383)%8192
此函数将每个页码映射为0到8191(含)之间的整数。每个32768字节shm文件单元的aHash字段将P值映射到同一单元的aPgno字段的索引,如下所示:
aPgno数组中的每个条目在aHash数组中都有一个对应的条目。aHash中的可用插槽比aPgno中的可用插槽更多。aHash中未使用的插槽填充为零。并且由于保证aHash中有未使用的时隙,因此这意味着可以保证计算X的循环终止。X的预期大小小于2。最坏的情况是X与aPgno中的条目数相同,在这种情况下,算法的运行速度与aPgno的线性扫描大致相同。但是那种最坏情况下的性能却极为罕见。通常,X的大小会很小,并且使用aHash数组可以使人们更快地计算FindFrame(P,M)。
这是描述哈希查找算法的另一种方法:从h =(P * 383)%8192开始,查看aHash [h]和后续的条目,当h达到8192时环绕到零,直到找到带有aHash [h] == 0。页数为P的所有aPgno条目将具有一个索引,该索引是因此计算出的aHash [h]值之一。但是,并非所有计算出的aHash [h]值都符合匹配条件,因此您必须独立检查它们。之所以会获得速度优势,是因为通常这组h值非常小。
请注意,shm文件的每个32768字节单元都有其自己的aHash和aPgno阵列。单个单元的aHash数组仅有助于查找同一单元中的aPgno条目。整个FindFrame(P,M)函数需要从最新的单元开始进行哈希查找,然后再回溯到最旧的单元,直到找到答案为止。
访问被同时使用由所述的XLOCK和xUnlock方法控制的遗留DELETE模式锁在WAL模式协调sqlite3_io_methods 对象和WAL锁具由的xShmLock方法控制 sqlite3_io_methods对象。
从概念上讲,只有一个DELETE模式锁。单个数据库连接的DELETE模式锁可以恰好处于以下状态之一:
DELETE模式锁存储在主数据库文件的锁字节页面上。仅SQLITE_LOCK_SHARED和SQLITE_LOCK_EXCLUSIVE是WAL模式数据库的因素。其他锁定状态在回滚模式下使用,但在WAL模式下不使用。
的WAL模式锁如上所述。
以下规则显示了如何使用每个锁。
SQLITE_LOCK_SHARED
连接到WAL模式数据库时,所有连接均连续保持SQLITE_LOCK_SHARED。对于读/写连接和只读连接都是如此。SQLITE_LOCK_SHARED锁甚至由不在事务内的连接保持。这与回滚模式不同,回滚模式在每个事务结束时都会释放SQLITE_LOCK_SHARED。
SQLITE_LOCK_EXCLUSIVE
在WAL模式和各种回滚模式之间进行切换时,连接将保留排他锁。当连接与WAL模式断开连接时,它们也可能会尝试获得EXCLUSIVE锁定。如果连接能够获得EXCLUSIVE锁,则意味着它是与数据库的唯一连接,因此它可能会尝试检查点,然后删除WAL-index和WAL文件。
当连接在主数据库上持有SHARED锁时,这将阻止任何其他连接获取EXCLUSIVE锁,从而防止WAL-index和WAL文件从其他用户下删除,并防止过渡到其他用户。其他用户以WAL模式访问数据库时的WAL模式。
WAL_WRITE_LOCK
WAL_WRITE_LOCK仅被独占锁定。永远不会在WAL_WRITE_LOCK上获得共享锁。
任何将内容附加到WAL末尾的连接都将保留EXCLUSIVE WAL_WRITE_LOCK。因此,一次只能有一个进程可以将内容附加到WAL。如果由于写入而发生WAL重置,则在保持此锁定的同时WAL-index标头的nBackfill字段将重置为零。
当连接在共享的WAL索引上运行恢复时,EXCLUSIVE还将保留WAL_WRITE_LOCK和其他几个锁定字节。
WAL_CKPT_LOCK
WAL_CKPT_LOCK仅被独占锁定。永远不会在WAL_CKPT_LOCK上获得共享锁。
任何运行检查点的连接都将保留EXCLUSIVE WAL_CKPT_LOCK 。按住此排他锁时,可以增加WAL-index标头的nBackfill字段,但不能减少。
当连接在共享的WAL索引上运行恢复时,EXCLUSIVE还将保留WAL_CKPT_LOCK和其他几个锁定字节。
WAL_RECOVER_LOCK
WAL_RECOVER_LOCK仅被独占锁定。永远不会在WAL_RECOVER_LOCK上获得共享锁。
运行恢复以重建共享WAL索引的任何连接都将保留EXCLUSIVE WAL_RECOVER_LOCK 。
正在重建其专用堆内存WAL索引的只读连接不持有此锁。(它不能,因为不允许只读连接持有任何排他锁。)仅当重建内存映射的SHM文件中包含的全局共享WAL索引时,才持有此锁。
除了锁定该字节外,运行恢复的连接还会获得除WAL_READ_LOCK(0)以外的所有其他WAL锁的排他锁。
WAL_READ_LOCK(N)
有五个单独的读取锁,编号为0到4。读取锁可以是SHARED或EXCLUSIVE。当连接处于事务中时,连接会在其中一个读锁字节上获得共享锁。当连接在更新相应读取标记的值时,连接也会在短暂的时间内获得一次读锁定的排他锁。运行恢复时,将仅保留读取锁1至4 。
每个读取锁定字节对应于位于WAL-index标头的字节100到119中的五个32位读取标记整数之一,如下所示:
锁名 | 锁偏移 | 读标记名称 | 读标记偏移 |
---|---|---|---|
WAL_READ_LOCK(0) | 123 | 读标记[0] | 100..103 |
WAL_READ_LOCK(1) | 124 | 读标记[1] | 104..107 |
WAL_READ_LOCK(2) | 125 | 读标记[2] | 108..111 |
WAL_READ_LOCK(3) | 126 | 读标记[3] | 112..115 |
WAL_READ_LOCK(4) | 127 | 读标记[4] | 116..119 |
当一个连接在WAL_READ_LOCK(N)上拥有一个共享锁时,这表示该连接将使用WAL而不是数据库文件中由第一个read-mark [N]条目修改的任何数据库页面的数据库文件。沃尔。读取标记[0]始终为零。如果连接持有WAL_READ_LOCK(0)上的共享锁,则意味着该连接希望能够忽略WAL并从主数据库中读取其想要的任何内容。如果N> 0,则连接可以随意使用WAL文件中的更多WAL文件(如果需要的话),最多可以读取前mxFrame帧。但是,当一个连接持有WAL_READ_LOCK(0)上的共享锁时,这将保证它永远不会从WAL中读取内容,而将直接从主数据库中获取所有内容。
当检查点运行时,如果看到WAL_READ_LOCK(N)上的锁,则它不得将WAL内容移入主数据库的时间超过最初的read-mark [N]帧。如果这样做,它将覆盖持有锁的进程原本希望能够从主数据库文件中读取的内容。如果这样的结果是:如果WAL文件包含的读取标记[N]个帧以上(如果mxFrame> read-mark [N]对于由另一个进程持有WAL_READ_LOCK(N)的任何读取标记),则检查点无法运行完成。
当编写者想要重置WAL时,必须确保N> 0时WAL_READ_LOCK(N)上没有锁定,因为此类锁定表明其他一些连接仍在使用当前WAL文件,并且WAL重置将从其中删除内容其他联系。如果其他连接正在保持WAL_READ_LOCK(0),则可以进行WAL重置,因为通过保持WAL_READ_LOCK(0),其他连接将保证不使用WAL中的任何内容。
进入和退出WAL模式
SQLITE_LOCK_EXCLUSIVE锁必须由要转换为退出WAL模式的连接持有。因此,就像其他任何写入事务一样,转换为WAL模式是因为回滚模式下的每个写入事务都需要SQLITE_LOCK_EXCLUSIVE锁。如果数据库文件已经处于WAL模式(因此希望将其更改回回滚模式),并且与数据库有两个或多个连接,则这些连接中的每一个都将持有SQLITE_LOCK_SHARED锁。这意味着无法获得SQLITE_LOCK_EXCLUSIVE,并且不允许从WAL模式过渡。这样可以防止一个连接从另一个连接中删除WAL模式。这也意味着将数据库从WAL模式转换为回滚模式的唯一方法是关闭除数据库之外的所有连接。
关闭与WAL模式数据库的连接
当数据库连接关闭时(通过sqlite3_close()或 sqlite3_close_v2()),将尝试获取SQLITE_LOCK_EXCLUSIVE。如果此尝试成功,则意味着正在关闭的连接是到数据库的最后一个连接。在那种情况下,最好清理WAL和WAL索引文件,因此关闭的连接运行一个检查点(在保持SQLITE_LOCK_EXCLUSIVE的同时),并删除WAL和WAL索引文件。在删除WAL和WAL索引文件之后,才释放SQLITE_LOCK_EXCLUSIVE。
如果应用程序在关闭之前在数据库连接上调用 sqlite3_file_control(SQLITE_FCNTL_PERSIST_WAL),则最终检查点仍将运行,但不会像通常那样删除WAL和WAL-index文件。这使数据库处于一种状态,该状态允许其他对数据库,WAL或WAL-index文件没有写许可权的进程以只读方式打开数据库。如果缺少WAL和WAL-index文件,则除非创建了不可变查询参数将该数据库指定为不可变的,否则缺少创建和初始化这些文件的权限的进程将无法打开数据库。
恢复期间重建全局共享的WAL索引
除WAL_READ_LOCK(0)外,所有WAL索引锁均在恢复期间重建全局共享WAL索引时排他地持有。
将新交易追加到WAL的末尾
在将新帧添加到WAL文件的末尾时,排他锁将保留在WAL_WRITE_LOCK上。
从数据库和WAL读取内容作为事务的一部分
运行检查点
重置WAL文件
一个WAL复位手段倒带WAL开始之初添加新的帧。在将新帧附加到mxFrame等于nBackfill并且在WAL_READ_LOCK(1)到WAL_READ_LOCK(4)上没有锁的WAL时,会发生这种情况。WAL_WRITE_LOCK被保持。
恢复是重建WAL索引以使其与WAL同步的过程。
恢复由第一个连接到WAL模式数据库的线程运行。恢复将还原WAL索引,以便它准确地描述WAL文件。如果在第一个线程连接到数据库时不存在WAL文件,则没有要恢复的内容,但是恢复过程仍在运行以初始化WAL索引。
如果WAL-index是作为内存映射文件实现的,并且该文件对要连接的第一个线程是只读的,则该线程将创建一个私有堆内存erazt WAL-index并运行恢复例程以填充该私有WAL -指数。产生相同的数据,但是将其私有保存,而不是写入公共共享存储区。
恢复是通过从头到尾对WAL进行一次遍历来进行的。读取WAL时,将在WAL的每个帧上验证校验和。扫描在文件末尾或第一个无效校验和处停止。该mxFrame字段设置为在WAL最后一个有效提交框架的指标。由于WAL帧号是从1开始索引的,因此mxFrame也是WAL中有效帧的数目。“提交帧”是在帧头的字节4到7中具有非零值的帧。由于恢复过程无法知道以前可能已经将WAL的多少帧复制回数据库,因此它将nBackfill值初始化为零。
在全局共享内存WAL索引的恢复过程中,排他锁通过WAL_READ_LOCK(4)保留在WAL_WRITE_LOCK,WAL_CKPT_LOCK,WAL_RECOVER_LOCK和WAL_READ_LOCK(1)上。换句话说,除WAL_READ_LOCK(0)以外,所有与WAL-index关联的锁均被保留。这样可以防止其他任何线程在恢复完成之前写入数据库和读取WAL中保留的任何事务。