小巧. 快速. 可靠.
三者择其二.
WAL 模式文件格式

本文档描述了 WAL 模式 在 unix 和 windows 上的实现细节。

单独的 文件格式 描述提供了有关数据库文件结构以及 WAL 模式 中使用的预写日志文件的详细信息。 但是,锁定协议和 WAL 索引格式的细节被故意省略,因为这些细节留给了各个 VFS 实现的自由裁量。

为了完整性,本文件中复制了一些与 WAL 模式处理相关的 文件格式 文档和其他地方包含的更高级别格式信息。

1. 磁盘上的文件

当处于活动使用状态时,WAL 模式数据库的状态由三个单独的文件描述

  1. 具有任意名称 "X" 的主数据库文件。
  2. 预写日志文件,通常命名为 "X-wal"。
  3. wal 索引文件,通常命名为 "X-shm"。

1.1. 主数据库文件

主数据库文件的格式如 文件格式 文档中所述。 主数据库中偏移量 18 和 19 处的 文件格式版本号 都必须为 2,以指示数据库处于 WAL 模式。 主数据库可以具有底层文件系统允许的任意名称。 不需要特殊的“文件后缀”,但“.db”,“.sqlite”和“.sqlite3”似乎是流行的选择。

1.2. 预写日志或 "-wal" 文件

预写日志或 "wal" 文件是一个前滚日志,它记录已提交但尚未应用于主数据库的事务。 关于 wal 文件格式的详细信息在主 文件格式 文档的 WAL 格式 部分中描述。 wal 文件的名称是通过将四个字符 "-wal" 附加到主数据库文件名称的末尾来命名的。 除了在 8+3 文件系统上,此类名称不被允许,在这种情况下,文件后缀将更改为“.WAL”。 但是,由于 8+3 文件系统越来越少见,因此通常可以忽略这种特殊情况。

1.3. WAL 索引或 "-shm" 文件

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 文件。

1.4. 文件生命周期

当 WAL 模式数据库处于活动使用状态时,通常所有三个以上文件都存在。 除了,如果设置了 独占锁定模式,则会省略 WAL 索引文件。

如果最后一个使用数据库的客户端通过调用 sqlite3_close() 正常关闭,则会自动运行 检查点,以将所有信息从 wal 文件传输到主数据库中,并且 shm 文件和 wal 文件都将被取消链接。 因此,当数据库没有被任何客户端使用时,通常只有主数据库文件存在于磁盘上。 但是,如果最后一个客户端在关闭之前没有调用 sqlite3_close(),或者如果最后一个断开连接的客户端是只读客户端,则最终的清理操作不会发生,并且 shm 和 wal 文件可能仍然存在于磁盘上,即使数据库没有使用。

1.5. 变体

当设置 PRAGMA locking_mode=EXCLUSIVE(独占锁定模式)时,一次只允许一个客户端打开数据库。 由于只有一个客户端可以使用数据库,因此会省略 shm 文件。 单个客户端使用堆内存中的缓冲区来代替内存映射的 shm 文件。

如果读/写客户端在关闭之前调用 sqlite3_file_control(SQLITE_FCNTL_PERSIST_WAL),则在关闭时仍然会运行检查点,但不会删除 shm 文件和 wal 文件。 这允许后续的只读客户端连接并读取数据库。

2. WAL 索引文件格式

WAL 索引或 "shm" 文件用于协调多个客户端对数据库的访问,并作为缓存,帮助客户端快速定位 wal 文件中的帧。

由于 shm 文件不参与恢复,因此 shm 文件不需要是机器字节序独立的。 因此,shm 文件中的数值以主机计算机的本机字节序写入,而不是像主数据库文件和 wal 文件那样转换为特定的跨平台字节序。

shm 文件包含一个或多个哈希表,其中每个哈希表的大小为 32768 字节。 除了,第一个哈希表的开头部分有 136 字节的头部,因此第一个哈希表的大小只有 32632 字节。 shm 文件的总大小始终是 32768 的倍数。 在大多数情况下,shm 文件的总大小恰好为 32768 字节。 只有在 wal 文件变得非常大(超过 4079 个帧)时,shm 文件才需要增长到超过单个哈希表。 由于默认的 自动检查点阈值 为 1000,因此 WAL 文件很少达到使 shm 文件增长所需的 4079 阈值。

2.1. WAL 索引头

shm 文件的前 136 字节是一个头部。 shm 头部有三个主要部分,如下所示

WAL 索引头部部分
字节描述
0..47WAL 索引信息的第一个副本
48..95WAL 索引信息的第二个副本
96..135检查点信息和锁

除了从 WAL 头部复制的盐值之外,shm 头部的各个字段都是主机机器的本机字节序中的无符号整数。 盐值是从 WAL 头部完全复制的,并且以 WAL 文件使用的任何字节序表示。 整数的大小可以是 8、16、32 或 64 位。 以下是 shm 头部各个字段的详细细分

WAL 索引头部详细信息
字节名称含义
0..3iVersion WAL 索引格式版本号。 始终为 3007000。
4..7  未使用的填充空间。 必须为零。
8..11iChange 无符号整数计数器,每次事务递增
12isInit "isInit" 标志。 当 shm 文件已初始化时为 1。
13bigEndCksum 如果 WAL 文件使用大端校验和,则为真。 如果 WAL 使用小端校验和,则为 0。
14..15szPage 数据库页大小(以字节为单位),如果页大小为 65536,则为 1。
16..19mxFrame WAL 文件中有效且已提交的帧数。
20..23nPage 数据库文件的大小(以页为单位)。
24..31aFrameCksum WAL 文件中最后一个帧的校验和。
32..39aSalt 从 WAL 文件头部复制的两个盐值。 这些值以 WAL 文件的字节序表示,这可能与机器的本机字节序不同。
40..47aCksum 对该头部中字节 0 到 39 的校验和。
48..95  该头部中字节 0 到 47 的副本。
96..99nBackfill 先前检查点已回填到数据库中的 WAL 帧数
100..119read-mark[0..4]五个“读取标记”。 每个读取标记都是一个 32 位无符号整数(4 字节)。
120..127  为 8 个文件锁预留的未使用空间。
128..132nBackfillAttempted 已尝试回填但可能未成功回填的 WAL 帧数。
132..136  为进一步扩展保留的未使用空间。

2.1.1. mxFrame 字段

偏移量 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。

2.1.2. nBackfill 字段

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 的情况下。

2.1.3. WAL 锁

头部中预留了 8 字节的空间,以使用 sqlite3_io_methods 对象中的 xShmLock() 方法支持文件锁定。 SQLite 从未读取或写入这 8 个字节,因为某些 VFS(例如:Windows)可能会使用强制性文件锁来实现锁。

以下是支持的 8 个锁

由 xShmLock() 控制的 WAL 索引锁
名称偏移量
xShmLock文件
WAL_WRITE_LOCK0 120
WAL_CKPT_LOCK1 121
WAL_RECOVER_LOCK2 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

待定:有关头部的更多信息

2.2. WAL-索引哈希表

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 字段的索引,如下所示:

  1. 计算哈希值: h = P * 383
  2. 令 X 为最大的一组连续整数 {h, h+1, h+2, ..., h+N},对于 X 中的每个 j,aPgno[j%8192]!=0。 如果 aPgno[h%8192]==0,则 X 集将为空。 通过从值 h%8192 开始,将 h%8192 添加到 X 并递增 h 直到遇到第一个 aPgno[h%8192] 条目为零,可以轻松计算 X 集。
  3. 集合 X 包含 shm 文件当前 32768 字节单元中可能成为 FindFrame(P,M) 函数解的每个条目的 aPgno 中的索引。 必须分别检查这些条目中的每一个,以确保 aPgno 值为 P 并且帧号不超过 M。 通过这两个测试的最大帧号是答案。

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) 函数需要从最新单元开始进行哈希查找,然后向后遍历到最旧单元,直到找到答案。

2.3. 锁定矩阵

在 WAL 模式下,使用 sqlite3_io_methods 对象的 xLock 和 xUnlock 方法控制的传统 DELETE 模式锁以及 sqlite3_io_methods 对象的 xShmLock 方法控制的 WAL 锁来协调访问。

从概念上讲,只有一个 DELETE 模式锁。 单个数据库连接的 DELETE 模式锁可以处于以下状态之一:

  1. SQLITE_LOCK_NONE(未锁定)
  2. SQLITE_LOCK_SHARED(读取)
  3. SQLITE_LOCK_RESERVED(读取,等待写入)
  4. SQLITE_LOCK_PENDING(新的读取器被阻塞,等待写入)
  5. SQLITE_LOCK_EXCLUSIVE(写入)

DELETE 模式锁存储在主数据库文件中的 锁字节页 上。 对于 WAL 模式数据库,只有 SQLITE_LOCK_SHARED 和 SQLITE_LOCK_EXCLUSIVE 是因素。 其他锁定状态在回滚模式下使用,但在 WAL 模式下不使用。

上面描述了 WAL 模式锁

2.3.1. 各种锁的使用方式

以下规则显示了每个锁的使用方式。

2.3.2. 需要锁的操作以及这些操作使用的锁

3. 恢复

恢复是重建 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