本文档描述了 WAL 模式 在 unix 和 windows 上的实现细节。
单独的 文件格式 描述提供了有关数据库文件结构以及 WAL 模式 中使用的预写日志文件的详细信息。 但是,锁定协议和 WAL 索引格式的细节被故意省略,因为这些细节留给了各个 VFS 实现的自由裁量。
为了完整性,本文件中复制了一些与 WAL 模式处理相关的 文件格式 文档和其他地方包含的更高级别格式信息。
当处于活动使用状态时,WAL 模式数据库的状态由三个单独的文件描述
主数据库文件的格式如 文件格式 文档中所述。 主数据库中偏移量 18 和 19 处的 文件格式版本号 都必须为 2,以指示数据库处于 WAL 模式。 主数据库可以具有底层文件系统允许的任意名称。 不需要特殊的“文件后缀”,但“.db”,“.sqlite”和“.sqlite3”似乎是流行的选择。
预写日志或 "wal" 文件是一个前滚日志,它记录已提交但尚未应用于主数据库的事务。 关于 wal 文件格式的详细信息在主 文件格式 文档的 WAL 格式 部分中描述。 wal 文件的名称是通过将四个字符 "-wal" 附加到主数据库文件名称的末尾来命名的。 除了在 8+3 文件系统上,此类名称不被允许,在这种情况下,文件后缀将更改为“.WAL”。 但是,由于 8+3 文件系统越来越少见,因此通常可以忽略这种特殊情况。
wal 索引文件或 "shm" 文件实际上没有用作文件。 相反,各个数据库客户端会映射 shm 文件并将其用作共享内存,以协调对数据库的访问并作为缓存,以便快速定位 wal 文件中的帧。 shm 文件的名称是主数据库文件名,后附加四个字符 "-shm"。 或者,对于 8+3 文件系统,shm 文件是主数据库文件,其后缀更改为“.SHM”。
shm 不包含任何数据库内容,并且不需要在崩溃后恢复数据库。 出于这个原因,第一个连接到静止数据库的客户端通常会在 shm 文件存在时截断该文件。 由于 shm 文件的内容不需要在崩溃中保存,因此 shm 文件从未 fsync() 到磁盘。 事实上,如果有一种机制可以让 SQLite 告诉操作系统不要将 shm 文件持久化到磁盘,而是始终将其保存在缓存内存中,SQLite 会使用该机制来避免与 shm 文件相关的任何不必要的磁盘 I/O。 但是,标准 posix 中不存在这种机制。
由于 shm 仅用于协调并发客户端之间的访问,因此如果设置了 独占锁定模式,则作为优化会省略 shm 文件。 当设置 独占锁定模式 时,SQLite 使用堆内存来代替内存映射的 shm 文件。
当 WAL 模式数据库处于活动使用状态时,通常所有三个以上文件都存在。 除了,如果设置了 独占锁定模式,则会省略 WAL 索引文件。
如果最后一个使用数据库的客户端通过调用 sqlite3_close() 正常关闭,则会自动运行 检查点,以将所有信息从 wal 文件传输到主数据库中,并且 shm 文件和 wal 文件都将被取消链接。 因此,当数据库没有被任何客户端使用时,通常只有主数据库文件存在于磁盘上。 但是,如果最后一个客户端在关闭之前没有调用 sqlite3_close(),或者如果最后一个断开连接的客户端是只读客户端,则最终的清理操作不会发生,并且 shm 和 wal 文件可能仍然存在于磁盘上,即使数据库没有使用。
当设置 PRAGMA locking_mode=EXCLUSIVE(独占锁定模式)时,一次只允许一个客户端打开数据库。 由于只有一个客户端可以使用数据库,因此会省略 shm 文件。 单个客户端使用堆内存中的缓冲区来代替内存映射的 shm 文件。
如果读/写客户端在关闭之前调用 sqlite3_file_control(SQLITE_FCNTL_PERSIST_WAL),则在关闭时仍然会运行检查点,但不会删除 shm 文件和 wal 文件。 这允许后续的只读客户端连接并读取数据库。
WAL 索引或 "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 | iVersion | WAL 索引格式版本号。 始终为 3007000。 |
4..7 | 未使用的填充空间。 必须为零。 | |
8..11 | iChange | 无符号整数计数器,每次事务递增 |
12 | isInit | "isInit" 标志。 当 shm 文件已初始化时为 1。 |
13 | bigEndCksum | 如果 WAL 文件使用大端校验和,则为真。 如果 WAL 使用小端校验和,则为 0。 |
14..15 | szPage | 数据库页大小(以字节为单位),如果页大小为 65536,则为 1。 |
16..19 | mxFrame | WAL 文件中有效且已提交的帧数。 |
20..23 | nPage | 数据库文件的大小(以页为单位)。 |
24..31 | aFrameCksum | WAL 文件中最后一个帧的校验和。 |
32..39 | aSalt | 从 WAL 文件头部复制的两个盐值。 这些值以 WAL 文件的字节序表示,这可能与机器的本机字节序不同。 |
40..47 | aCksum | 对该头部中字节 0 到 39 的校验和。 |
48..95 | 该头部中字节 0 到 47 的副本。 | |
96..99 | nBackfill | 先前检查点已回填到数据库中的 WAL 帧数 |
100..119 | read-mark[0..4] | 五个“读取标记”。 每个读取标记都是一个 32 位无符号整数(4 字节)。 |
120..127 | 为 8 个文件锁预留的未使用空间。 | |
128..132 | nBackfillAttempted | 已尝试回填但可能未成功回填的 WAL 帧数。 |
132..136 | 为进一步扩展保留的未使用空间。 |
偏移量 16(以及在偏移量 64 处重复)处的 32 位无符号整数是 WAL 中有效帧数。 因为 WAL 帧从 1 开始编号,所以 mxFrame 也是 WAL 中最后一个有效提交帧的索引。 提交帧是指帧在其帧头部中字节 4 到 7 处具有非零“数据库大小”值,并且该值指示事务的结束。
当 mxFrame 字段为零时,表示 WAL 为空,并且所有内容应直接从数据库文件中获取。
当 mxFrame 等于 nBackfill 时,表示 WAL 中的所有内容都已写入数据库。 在这种情况下,所有内容都可以直接从数据库中读取。 此外,如果没有任何连接持有 WAL_READ_LOCK(N) 的锁,其中 N>0,则下一个写入者可以自由地 重置 WAL。
mxFrame 值始终大于或等于 nBackfill 和 nBackfillAttempted。
WAL 索引头部中偏移量 128 处的 32 位无符号整数称为“nBackfill”。 该字段保存已复制回主数据库的 WAL 文件中的帧数。
nBackfill 数值永远不会大于 mxFrame。 当 nBackfill 等于 mxFrame 时,表示 WAL 内容已完全写入数据库,并且如果没有任何连接持有任何 WAL_READ_LOCK(N) 的锁,其中 N>0,则可以 重置 WAL。
nBackfill 只能在持有 WAL_CKPT_LOCK 的情况下增加。 但是,在 WAL 重置 期间,nBackfill 将更改为零,并且发生在持有 WAL_WRITE_LOCK 的情况下。
头部中预留了 8 字节的空间,以使用 sqlite3_io_methods 对象中的 xShmLock() 方法支持文件锁定。 SQLite 从未读取或写入这 8 个字节,因为某些 VFS(例如:Windows)可能会使用强制性文件锁来实现锁。
以下是支持的 8 个锁
名称 | 偏移量 | |
---|---|---|
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 |
待定:有关头部的更多信息
shm 文件中的哈希表旨在快速回答以下问题。
FindFrame(P,M): 给定一个页号 P 和一个最大的 WAL 帧索引 M,返回页 P 的不超过 M 的最大 WAL 帧索引,如果页 P 没有不超过 M 的帧,则返回 NULL。
令数据类型 "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 字节哈希表 (n>1) 的 aPgno[k] 条目存储了 WAL 文件中第 (k+4062+4096*(n-2)) 帧的数据库页号。
这里有一个稍微不同的方法来描述 aPgno 值: 如果你将所有 aPgno 值视为一个连续的数组,那么 WAL 文件中第 i 帧存储的数据库页号存储在 aPgno[i] 中。 当然,aPgno 不是一个连续的数组。 前 4062 个条目在 shm 文件的第一个 32768 字节单元上,后续的值在 shm 文件后面单元的 4096 个条目块中。
计算 FindFrame(P,M) 的一种方法是从 aPgno 数组的第 M 个条目开始扫描,向后遍历到开头,并返回 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) 函数需要从最新单元开始进行哈希查找,然后向后遍历到最旧单元,直到找到答案。
在 WAL 模式下,使用 sqlite3_io_methods 对象的 xLock 和 xUnlock 方法控制的传统 DELETE 模式锁以及 sqlite3_io_methods 对象的 xShmLock 方法控制的 WAL 锁来协调访问。
从概念上讲,只有一个 DELETE 模式锁。 单个数据库连接的 DELETE 模式锁可以处于以下状态之一:
DELETE 模式锁存储在主数据库文件中的 锁字节页 上。 对于 WAL 模式数据库,只有 SQLITE_LOCK_SHARED 和 SQLITE_LOCK_EXCLUSIVE 是因素。 其他锁定状态在回滚模式下使用,但在 WAL 模式下不使用。
上面描述了 WAL 模式锁。
以下规则显示了每个锁的使用方式。
SQLITE_LOCK_SHARED
所有连接在附加到 WAL 模式数据库时始终保持 SQLITE_LOCK_SHARED。 这对于读写连接和只读连接都是正确的。 即使对于不在事务中的连接,也始终持有 SQLITE_LOCK_SHARED 锁。 这与回滚模式不同,在回滚模式中,SQLITE_LOCK_SHARED 在每个事务结束时都会释放。
SQLITE_LOCK_EXCLUSIVE
当连接在 WAL 模式和各种回滚模式之间切换时,连接会持有独占锁。 当连接断开 WAL 模式时,连接也可能尝试获取独占锁。 如果连接能够获取独占锁,则意味着它是数据库的唯一连接,因此它可以尝试执行检查点,然后删除 WAL 索引和 WAL 文件。
当连接在主数据库上持有共享锁时,这将阻止任何其他连接获取独占锁,从而阻止 WAL 索引和 WAL 文件在其他用户不知情的情况下被删除,并阻止在其他用户以 WAL 模式访问数据库时退出 WAL 模式。
WAL_WRITE_LOCK
WAL_WRITE_LOCK 仅以独占方式锁定。 从未对 WAL_WRITE_LOCK 进行共享锁定。
任何将内容追加到 WAL 末尾的连接都会持有独占 WAL_WRITE_LOCK。 因此,一次只有一个进程可以将内容追加到 WAL。 如果由于写入而发生 WAL 重置,则在持有此锁时,WAL 索引头的 nBackfill 字段将重置为零。
当连接在共享 WAL 索引上运行 恢复 时,也会持有独占 WAL_WRITE_LOCK 以及其他几个锁定字节。
WAL_CKPT_LOCK
WAL_CKPT_LOCK 仅以独占方式锁定。 从未对 WAL_CKPT_LOCK 进行共享锁定。
任何正在运行 检查点 的连接都会持有独占 WAL_CKPT_LOCK。 在持有此独占锁时,可以增加 WAL 索引头的 nBackfill 字段,但不能减少它。
当连接在共享 WAL 索引上运行 恢复 时,也会持有独占 WAL_CKPT_LOCK 以及其他几个锁定字节。
WAL_RECOVER_LOCK
WAL_RECOVER_LOCK 仅以独占方式锁定。 从未对 WAL_RECOVER_LOCK 进行共享锁定。
任何正在运行 恢复 以重建共享 WAL 索引的连接都会持有独占 WAL_RECOVER_LOCK。
重建其私有堆内存 WAL 索引的只读连接不会持有此锁。 (它不能持有,因为只读连接不允许持有任何独占锁。) 此锁仅在重建包含在内存映射 SHM 文件中的全局共享 WAL 索引时持有。
除了锁定此字节外,运行 恢复 的连接还会获得除 WAL_READ_LOCK(0) 之外所有其他 WAL 锁的独占锁。
WAL_READ_LOCK(N)
有五个独立的读取锁,编号为 0 到 4。 读取锁可以是共享的或独占的。 连接在事务中时,将在其中一个读取锁字节上获得共享锁。 连接也会以独占方式获得读取锁,一次一个,在更新相应读取标记的值的短暂时间内。 当运行 恢复 时,读取锁 1 到 4 会以独占方式持有。
每个读取锁字节对应于 WAL 索引头中字节 100 到 119 中的五个 32 位读取标记整数之一,如下所示:
锁名称 | 锁偏移量 | 读取标记名称 | 读取标记偏移量 |
---|---|---|---|
WAL_READ_LOCK(0) | 123 | read-mark[0] | 100..103 |
WAL_READ_LOCK(1) | 124 | read-mark[1] | 104..107 |
WAL_READ_LOCK(2) | 125 | read-mark[2] | 108..111 |
WAL_READ_LOCK(3) | 126 | read-mark[3] | 112..115 |
WAL_READ_LOCK(4) | 127 | read-mark[4] | 116..119 |
当连接在 WAL_READ_LOCK(N) 上持有共享锁时,这意味着连接承诺它将使用 WAL 而不是数据库文件来访问由 WAL 中的第一个 read-mark[N] 条目修改的任何数据库页。 read-mark[0] 始终为零。 如果连接在 WAL_READ_LOCK(0) 上持有共享锁,则意味着连接期望能够忽略 WAL 并从主数据库中读取任何它想要的内容。 如果 N>0,则连接可以自由地在它想要的情况下使用 WAL 文件中超出 read-mark[N] 的更多内容,直到第一个 mxFrame 帧。 但当连接在 WAL_READ_LOCK(0) 上持有共享锁时,这意味着它承诺它永远不会从 WAL 读取内容,并且会直接从主数据库中获取所有内容。
当检查点运行时,如果它看到 WAL_READ_LOCK(N) 上的锁,那么它不得将 WAL 内容移入主数据库超过第一个 read-mark[N] 帧。 如果这样做,它将覆盖持有锁的进程期望能够从主数据库文件中读出的内容。 这将导致如果 WAL 文件包含超过 read-mark[N] 帧的内容 (如果对于任何持有 WAL_READ_LOCK(N) 锁的进程,mxFrame>read-mark[N],那么检查点就无法完成。
当写入器想要 重置 WAL 时,它必须确保对于 N>0,WAL_READ_LOCK(N) 上没有锁,因为这些锁表示其他一些连接仍在使用当前 WAL 文件,而 WAL 重置 将删除这些其他连接的内容。 如果其他连接持有 WAL_READ_LOCK(0),那么 WAL 重置 就可以进行,因为通过持有 WAL_READ_LOCK(0),这些其他连接承诺不使用 WAL 中的任何内容。
进入和退出 WAL 模式
想要进入或退出 WAL 模式的连接必须持有 SQLITE_LOCK_EXCLUSIVE 锁。因此,进入 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-索引文件不会像通常那样被删除。这将数据库置于一种状态,允许其他进程在没有数据库、WAL 或 WAL-索引文件的写入权限的情况下以只读方式打开数据库。如果 WAL 和 WAL-索引文件丢失,那么没有创建和初始化这些文件的权限的进程将无法打开数据库,除非数据库使用 不可变查询参数 指定为不可变。
在 恢复 期间重建全局共享 WAL-索引
在 恢复 期间重建全局共享 WAL-索引时,所有 WAL-索引锁(WAL_READ_LOCK(0) 除外)都将被独占持有。
将新事务追加到 WAL 的末尾
在将新帧添加到 WAL 文件末尾时,将对 WAL_WRITE_LOCK 持有独占锁。
从数据库和 WAL 中读取内容作为事务的一部分
运行检查点
重置 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-索引作为内存映射文件实现,并且该文件对第一个连接的线程是只读的,那么该线程将创建一个私有的堆内存 ersazt WAL-索引,并运行恢复例程来填充该私有的 WAL-索引。结果数据相同,但它是私有持有的,而不是写入公共共享内存区域。
恢复通过对 WAL 从头到尾进行单次遍历来完成。在读取每个 WAL 帧时都会验证校验和。扫描在文件末尾或第一个无效校验和处停止。 mxFrame 字段设置为 WAL 中最后一个有效提交帧的索引。由于 WAL 帧号从 1 开始索引,因此 mxFrame 也是 WAL 中有效帧的数量。"提交帧" 是在帧标头字节 4 到 7 中具有非零值的帧。由于恢复过程无法知道先前可能已将多少个 WAL 帧复制回数据库,因此它将 nBackfill 值初始化为零。
在全局共享内存 WAL-索引的恢复期间,将对 WAL_WRITE_LOCK、WAL_CKPT_LOCK、WAL_RECOVER_LOCK 和 WAL_READ_LOCK(1) 到 WAL_READ_LOCK(4) 持有独占锁。换句话说,与 WAL-索引关联的所有锁(WAL_READ_LOCK(0) 除外)都将被独占持有。这将阻止任何其他线程写入数据库并读取 WAL 中持有的任何事务,直到恢复完成。
此页面上次修改于 2022-01-08 05:02:57 UTC