本文档描述了有关如何在 unix 和 windows 上实现 WAL 模式的低级详细信息。
单独的文件格式描述提供了有关数据库文件结构和WAL 模式中使用的写头日志文件的详细信息 。但是锁定协议的细节和 WAL 索引的格式被故意省略,因为这些细节留给各个VFS实现来决定。本文档填补了 unix 和 windows VFSes缺失的细节。
为了完整起见,当涉及 WAL 模式处理时, 文件格式文档和其他地方包含的一些更高级别的格式化信息在这里被复制。
1.磁盘上的文件
在使用中,WAL 模式数据库的状态由三个单独的文件描述:
- 具有任意名称“X”的主数据库文件。
- 预写日志文件,通常命名为“X-wal”。
- wal-index 文件,通常命名为“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-Index 或“-shm”文件
wal-index 文件或“shm”文件实际上并不用作文件。相反,各个数据库客户端映射 shm 文件并将其用作共享内存以协调对数据库的访问,并将其用作高速缓存以在 wal 文件中快速定位框架。shm 文件的名称是主数据库文件名加上四个字符“-shm”。或者,对于 8+3 文件系统,shm 文件是主数据库文件,后缀更改为“.SHM”。
shm 不包含任何数据库内容,并且不需要在崩溃后恢复数据库。因此,第一个连接到静态数据库的客户端通常会截断 shm 文件(如果存在)。由于 shm 文件的内容不需要在崩溃时保留,因此 shm 文件永远不会 fsync()-ed 到磁盘。事实上,如果有一种机制让 SQLite 可以告诉操作系统永远不要将 shm 文件保存到磁盘,而是始终将其保存在缓存内存中,SQLite 将使用该机制来避免与 shm 文件相关的任何不必要的磁盘 I/O . 但是,标准 posix 中不存在这样的机制。
由于 shm 仅用于协调并发客户端之间的访问,因此如果设置了独占锁定模式 ,则省略 shm 文件作为优化。设置独占锁定模式时,SQLite 使用堆内存代替内存映射 shm 文件。
1.4. 文件生命周期
当 WAL 模式数据库处于活动状态时,上述所有三个文件通常都存在。除非,如果设置了独占锁定模式,则省略 Wal-Index 文件 。
如果使用数据库的最后一个客户端通过调用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-index 或“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 header主要分为以下三个部分:
Bytes | Description |
---|---|
0..47 | First copy of the WAL Index Information |
48..95 | Second copy of the WAL Index Information |
96..135 | Checkpoint Information and Locks |
shm 标头的各个字段,除了从 WAL 标头复制的 salt 值外,都是主机本机字节顺序中的无符号整数。salt 值是 WAL 标头的精确副本,并且采用 WAL 文件使用的任何字节顺序。整数的大小可以是 8、16、32 或 64 位。shm 标头各个字段的详细分类如下:
Bytes | Name | Meaning |
---|---|---|
0..3 | iVersion | The WAL-index format version number. Always 3007000. |
4..7 | Unused padding space. Must be zero. | |
8..11 | iChange | Unsigned integer counter, incremented with each transaction |
12 | isInit | The "isInit" flag. 1 when the shm file has been initialized. |
13 | bigEndCksum | True if the WAL file uses big-ending checksums. 0 if the WAL uses little-endian checksums. |
14..15 | szPage | The database page size in bytes, or 1 if the page size is 65536. |
16..19 | mxFrame | Number of valid and committed frames in the WAL file. |
20..23 | nPage | Size of the database file in pages. |
24..31 | aFrameCksum | Checksum of the last frame in the WAL file. |
32..39 | aSalt | The two salt value copied from the WAL file header. These values are in the byte-order of the WAL file, which might be different from the native byte-order of the machine. |
40..47 | aCksum | A checksum over bytes 0 through 39 of this header. |
48..95 | A copy of bytes 0 through 47 of this header. | |
96..99 | nBackfill | Number of WAL frames that have already been backfilled into the database by prior checkpoints |
100..119 | read-mark[0..4] | Five "read marks". Each read mark is a 32-bit unsigned integer (4 bytes). |
120..127 | Unused space set aside for 8 file locks. | |
128..132 | nBackfillAttempted | Number of WAL frames that have attempted to be backfilled but which might not have been backfilled successfully. |
132..136 | Unused space reserved for further expansion. |
2.1.1. mxFrame 字段
偏移量 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。
2.1.2. nBackfill 字段
WAL 索引标头中偏移量 128 处的 32 位无符号整数称为“nBackfill”。该字段包含 WAL 文件中已复制回主数据库的帧数。
nBackfill 数永远不会大于mxFrame。当 nBackfill 等于mxFrame时,这意味着 WAL 内容已完全写回到数据库中,如果 N>0 的任何 WAL_READ_LOCK(N) 上都没有锁定,则 可以重置 WAL 。
只能在持有 WAL_CKPT_LOCK 时增加 nBackfill。然而,nBackfill 在WAL 重置期间更改为零,并且这发生在持有 WAL_WRITE_LOCK 的情况下。
2.1.3. WAL 锁
在标头中预留了八个字节的空间,以支持使用sqlite3_io_methods 对象中的 xShmLock() 方法进行文件锁定。SQLite 永远不会读取或写入这八个字节,因为某些 VFS(例如:Windows)可能会使用强制文件锁来实现锁定。
这些是支持的八个锁:
Name | Offset | |
---|---|---|
xShmLock | File | |
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 |
待定:有关标头的更多信息
2.2. WAL-索引哈希表
shm 文件中的哈希表旨在快速回答以下问题:
FindFrame(P,M):给定一个页码P和一个最大WAL帧索引M,返回页面P最大的不超过M的WAL帧索引,如果页面P没有不超过M的帧则返回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 数组,然后向后工作并返回 J,其中 aPgno[J]==P。这样的算法会起作用,并且比在整个 WAL 文件中搜索页码为 P 的最新帧要快。但是使用 aHash 结构可以使搜索速度更快。
使用以下散列函数将数据库页码 P 映射到散列值:
h = (P * 383)%8192
此函数将每个页码映射为 0 到 8191 之间的整数(含 0 和 8191)。每个 32768 字节的 shm 文件单元的 aHash 字段将 P 值映射到同一单元的 aPgno 字段的索引中,如下所示:
- 计算哈希值:h = P * 383
- 令 X 为最大的一组连续整数 {h, h+1, h+2, ..., h+N} 使得对于 X 中的每个 j,aPgno[j%8192]!=0。如果 aPgno[h%8192]==0,则 X 集将为空。X 集很容易计算,方法是从值 h%8192 开始,然后将 h%8192 添加到 X 并递增 h,直到遇到第一个为零的 aPgno[h%8192] 条目。
- 集合 X 包含 shm 文件当前 32768 字节单元中每个条目在 aPgno 中的索引,这可能是 FindFrame(P,M) 函数的一个解决方案。必须分别检查这些条目中的每一个,以确保 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. 锁定矩阵
使用由sqlite3_io_methods对象的 xLock 和 xUnlock 方法控制的遗留 DELETE 模式锁和由sqlite3_io_methods 对象的 xShmLock 方法控制的 WAL 锁, 在 WAL 模式下协调访问。
从概念上讲,只有一个 DELETE 模式锁。单个数据库连接的 DELETE 模式锁可以恰好处于以下状态之一:
- SQLITE_LOCK_NONE(解锁)
- SQLITE_LOCK_SHARED(读)
- SQLITE_LOCK_RESERVED(读,等待写)
- SQLITE_LOCK_PENDING(新读者阻塞,等待写入)
- SQLITE_LOCK_EXCLUSIVE(写)
DELETE 模式锁存储在主数据库文件的锁字节页上。只有 SQLITE_LOCK_SHARED 和 SQLITE_LOCK_EXCLUSIVE 是 WAL 模式数据库的因素。其他锁定状态用于回滚模式,但不用于 WAL 模式。
上面描述 了WAL 模式锁。
2.3.1. 各种锁的使用方法
以下规则显示了每个锁的使用方式。
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 上永远不会有共享锁。
EXCLUSIVE WAL_WRITE_LOCK 由任何将内容附加到 WAL 末尾的连接持有。因此,一次只有一个进程可以将内容附加到 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 header的nBackfill字段可以在持有这个独占锁时增加,但不能减少。
当连接在共享 WAL 索引上 运行恢复时,EXCLUSIVE 也被 WAL_CKPT_LOCK 和其他几个锁定字节持有。
WAL_RECOVER_LOCK
WAL_RECOVER_LOCK 仅被独占锁定。WAL_RECOVER_LOCK 上永远不会有共享锁。
EXCLUSIVE WAL_RECOVER_LOCK 由任何运行 恢复以重建共享 WAL 索引的连接持有。
正在重建其私有堆内存 WAL 索引的只读连接不持有此锁。(它不能,因为只读连接不允许持有任何独占锁。)只有在重建内存映射 SHM 文件中包含的全局共享 WAL 索引时才会持有此锁。
除了锁定这个字节之外,运行恢复的连接还会获得除 WAL_READ_LOCK(0) 之外的所有其他 WAL 锁的独占锁。
WAL_READ_LOCK(N)
有五个独立的读锁,编号为 0 到 4。读锁可以是 SHARED 或 EXCLUSIVE。当连接在事务中时,它们会在其中一个读锁字节上获得共享锁。连接还会在更新相应读取标记的值时的短暂时刻获得读取锁的独占锁,一次一个。读锁 1 到 4 在运行recovery时被独占持有。
每个读取锁定字节对应于位于 WAL 索引标头的字节 100 到 119 中的五个 32 位读取标记整数之一,如下所示:
Lock Name Lock offset Read-mark name Read-mark offset 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 而不是数据库文件,用于由第一个 read-mark[N] 条目修改的任何数据库页面瓦尔。读取标记[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] 帧。如果这样做,它将覆盖持有锁的进程期望能够从主数据库文件中读出的内容。如果这是 f WAL 文件包含多于 read-mark[N] 帧的结果(如果 mxFrame>read-mark[N] 对于另一个进程持有 WAL_READ_LOCK(N) 的任何读标记),则检查点无法运行完成。
当作者想要重置 WAL时,它必须确保 WAL_READ_LOCK(N) 上没有锁,因为 N>0 因为这样的锁表明其他一些连接仍在使用当前的 WAL 文件,并且WAL 重置会从中删除内容那些其他的联系。如果其他连接持有 WAL_READ_LOCK(0),则WAL 重置是可以的,因为通过持有 WAL_READ_LOCK(0),其他连接承诺不使用 WAL 中的任何内容。
2.3.2. 需要锁并锁定这些操作的操作使用
进入和退出 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-index 文件,因此关闭连接会运行一个检查点(同时保持 SQLITE_LOCK_EXCLUSIVE)并删除 WAL 和 WAL-index 文件。SQLITE_LOCK_EXCLUSIVE 直到 WAL 和 WAL-index 文件都被删除后才会被释放。
如果应用程序在关闭之前在数据库连接上调用 sqlite3_file_control ( SQLITE_FCNTL_PERSIST_WAL ),那么最后的检查点仍会运行,但 WAL 和 WAL-index 文件不会像通常那样被删除。这使数据库处于允许其他进程对数据库、WAL 或 WAL 索引文件没有写权限的状态,以只读方式打开数据库。如果缺少 WAL 和 WAL-index 文件,则没有创建和初始化这些文件权限的进程将无法打开数据库,除非使用不可变查询参数将数据库指定为不可变。
在恢复期间重建全局共享 WAL 索引
所有 WAL 索引锁,除了 WAL_READ_LOCK(0),在recovery期间重建全局共享 WAL 索引时被独占持有。
在 WAL 的末尾追加一个新事务
在 WAL_WRITE_LOCK 上持有独占锁,同时将新帧添加到 WAL 文件的末尾。
作为事务的一部分从数据库和 WAL 中读取内容
运行检查点
重置 WAL 文件
WAL 重置意味着倒回 WAL 并在开头开始添加新帧。当将新帧附加到具有等于nBackfill的mxFrame并且在 WAL_READ_LOCK(1) 到 WAL_READ_LOCK(4) 上没有锁定的 WAL 时,会发生这种情况。WAL_WRITE_LOCK 被持有。
3.恢复
恢复是重建 WAL 索引的过程,以便它与 WAL 同步。
恢复由连接到 WAL 模式数据库的第一个线程运行。恢复恢复 WAL 索引,以便它准确地描述 WAL 文件。如果第一个线程连接到数据库时不存在 WAL 文件,则无需恢复,但恢复过程仍会运行以初始化 WAL 索引。
如果 WAL-index 实现为内存映射文件并且该文件对于第一个连接线程是只读的,那么该线程创建一个私有堆内存 ersazt WAL-index 并运行恢复例程来填充该私有 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 索引关联的锁都是独占的。这会阻止任何其他线程写入数据库并读取 WAL 中保存的任何事务,直到恢复完成。