Sqlite Wal Design
SQLite WAL (Write-Ahead Logging) 设计原理详解
一、源码文件位置
核心实现文件
src/wal.c- WAL 主实现 (约 4700 行代码)src/pager.c- WAL 相关的 Pager 层逻辑
配套文件
src/wal.h- WAL 接口定义src/os_unix.c/src/os_win.c- 平台特定的 WAL 实现
源码注释位置
文件开头部分 (行 1-250) 有详细的 WAL 格式说明文档
二、WAL 核心数据结构
2.1 WAL Header (32 字节)
struct {
u32 magic; /* 0: 0x377f0682 (小端) 或 0x377f0683 (大端) */
u32 version; /* 4: 文件格式版本,当前 3007000 */
u32 pageSize; /* 8: 数据库页面大小,如 1024、4096 等 */
u32 ckptSeq; /* 12: Checkpoint 序列号 */
u32 salt1; /* 16: Salt-1,随机整数,每次 checkpoint 递增 */
u32 salt2; /* 20: Salt-2,随机整数,每次 checkpoint 随机化 */
u32 checksum1; /* 24: 头部校验和 - 第一部分 */
u32 checksum2; /* 28: 头部校验和 - 第二部分 */
} WalIndexHdr;
属性说明:
- Magic Number:
0x377f0682(小端序) 或0x377f0683(大端序) - Frame Count: WAL 可包含任意数量的帧
- Salt Mechanism: 防止旧帧被误认为是新帧
- Checksum: 使用 Fibonnaci 反向权重算法
2.2 WAL Frame (24 字节 Header + PageData)
struct {
u32 pageNum; /* 0: 页面号 */
u32 dbSize; /* 4: Commit Marker 时显示数据库大小 (页面数) */
u32 salt1; /* 8: 从 WAL Header 拷贝 */
u32 salt2; /* 12: 从 WAL Header 拷贝 */
u32 checksum1; /* 16: 帧校验和 - 第一部分 */
u32 checksum2; /* 20: 帧校验和 - 第二部分 */
u8 pageData[]; /* 24: 实际页面数据,大小为 pageSize */
} WalFrame;
有效帧验证条件:
- salt1、salt2 与 Header 匹配
- checksum1、checksum2 与实际数据一致
2.3 Wal 内部状态结构
struct Wal {
sqlite3_vfs *pVfs; /* VFS 模块 */
sqlite3_file *pWalFd; /* WAL 文件句柄 */
sqlite3_file *pDbFd; /* 数据库文件句柄 */
int readLock; /* Reader 锁 ID */
int writeLock; /* Writer 锁标志 */
int ckptLock; /* Checkpoint 锁标志 */
int exclusiveMode; /* 锁定模式 */
WalIndexHdr hdr; /* 本地 WAL Header 缓存 */
u32 aWiData[WAL_NREADER]; /* 共享内存数据 */
int syncHeader; /* 是否同步 header */
int padToSectorBoundary; /* 是否对齐扇区边界 */
i64 mxWalSize; /* WAL 最大大小限制 */
u32 iCallback; /* 事务回调帧数 */
u32 iReCksum; /* 重计算的位置 */
};
2.4 WalCkptInfo (检查点信息)
struct WalCkptInfo {
u32 nBackfill; /* 已刷入数据库的帧数 */
u32 aReadMark[WAL_NREADER]; /* 读器标记 */
u8 aLock[SQLITE_SHM_NLOCK]; /* 锁字节 */
u32 nBackfillAttempted; /* 尝试刷入的帧数 */
u32 notUsed0; /* 预留字段 */
};
三、WAL 工作机制详解
3.1 写入流程
1. 打开 WAL 连接 → sqlite3WalOpen()
2. 开始写入事务 → sqlite3WalBeginWriteTransaction()
- 获取 WAL_WRITE_LOCK (单 writer 锁)
- 检查 WAL Header 一致性
3. 写入页面帧 → sqlite3WalFrames()
- 计算 checksum
- 追加写入到 WAL 文件末尾
- 更新 wal-index 中的 frame 映射
4. Commit 提交 → FSync + 写 Commit Marker
5. 释放写锁 → sqlite3WalEndWriteTransaction()
核心代码片段:
int sqlite3WalBeginWriteTransaction(Wal *pWal) {
/* 单 writer 锁,其他 writer 等待或返回 BUSY */
rc = walLockExclusive(pWal, WAL_WRITE_LOCK, 1);
/* 防止 WAL Header 被其他进程修改 */
if (memcmp(&pWal->hdr, (void *)walIndexHdr(pWal), sizeof(WalIndexHdr)) != 0) {
return SQLITE_BUSY_SNAPSHOT;
}
return SQLITE_OK;
}
3.2 读取流程
1. 开启读事务 → sqlite3WalBeginReadTransaction()
- 获取 WAL_READ_LOCK(k)
- 记录当前 mxFrame 作为快照
2. 查找页面 → sqlite3WalFindFrame()
- 通过 wal-index 定位帧位置
- 若找到有效帧则读取
- 否则从数据库文件读取
3. 读取数据 → sqlite3WalReadFrame()
4. 释放读锁 → 释放 WAL_READ_LOCK(k)
Snapshot 读机制:
- Reader 记录 mxFrame (当前 WAL 中最大有效帧)
- 后续读取时忽略 mxFrame 之后的新帧
- 实现 快照隔离 (Snapshot Isolation)
3.3 Checkpoint 流程
PRAGMA wal_checkpoint; -- PASSIVE 模式
PRAGMA wal_checkpoint(TRUNCATE); -- RESTART 模式
PRAGMA wal_checkpoint(FULL); -- FULL 模式
4 种 Checkpoint 模式:
| 模式 | 功能 | 是否阻塞读 | 是否阻塞写 | WAL 处理 |
|---|---|---|---|---|
PASSIVE |
被动 checkpoint | ❌ | ❌ | 仅刷已提交的帧 |
FULL |
完整 checkpoint | ❌ | ✅ | 刷所有帧,不重启 WAL |
RESTART |
重启 WAL | ✅ | ✅ | 刷所有帧,截断 WAL |
TRUNCATE |
截断模式 | ✅ | ✅ | 最小化 WAL 文件 |
Checkpoint 执行步骤:
int sqlite3WalCheckpoint(Wal *pWal, int eMode) {
// 1. 获取 checkpoint 锁
walLockExclusive(pWal, WAL_CKPT_LOCK, 1);
// 2. FULL/RESTART/TRUNCATE 模式获取 writer 锁
walBusyLock(pWal, ..., WAL_WRITE_LOCK, 1);
// 3. 读取 wal-index 头部信息
walIndexReadHdr(pWal, &isChanged);
// 4. 将 WAL 帧刷入数据库文件
walCheckpoint(pWal, db, eMode2, ...);
// 5. 释放所有锁
walUnlockExclusive(pWal, WAL_CKPT_LOCK, 1);
}
四、文件系统布局
4.1 三文件设计
数据库目录结构:
.
├── mydb # 主数据库文件 (通用 header + 页数据)
├── mydb-wal # WAL 预写日志文件
└── mydb-shm # 共享内存索引文件 (wal-index)
关系说明:
mydb: 数据库主体,包含 Schema 和用户数据mydb-wal: 预写日志,记录修改后的页面帧mydb-shm: 共享内存,提供快速查找 WAL 帧位置
4.2 共享内存 (wal-index) 结构
/* 第一部分:Header (136 字节) */
- WalIndexHdr (x2) : 重复两个,增强可靠性
- WalCkptInfo : Checkpoint 信息
/* 第二部分:Index Blocks (4096 条目/块) */
- Page Mapping: 帧号到页面号的映射
- Hash Table: 快速定位帧的哈希表
查找算法:
int sqlite3WalFindFrame(Wal *pWal, Pgno pgNo, int *piFrame) {
// 1. 哈希表查找
hashLoc = walHashLocation(pgNo);
// 2. 从 wal-index 中定位帧
for (每个 index block) {
if (pgNo == pageMapping[i]) {
*piFrame = i;
return SQLITE_OK;
}
}
return SQLITE_NOTFOUND;
}
五、并发控制机制
5.1 Lock 类型 (共享内存区域)
WAL_INDEX_LOCK_OFFSET = 120 字节
120: WAL_WRITE_LOCK -- 写入锁 (单 writer)
121: WAL_CKPT_LOCK -- Checkpoint 锁
122: WAL_RECOVER_LOCK -- 崩溃恢复锁
123-127: WAL_READ_LOCK[0-4] -- Reader 锁 (最多 5 个并发 reader)
5.2 锁协议
Reader 协议:
1. 获取 WAL_READ_LOCK(k) (共享锁,非阻塞尝试)
2. 记录 mxFrame 到 aReadMark[k]
3. 读取数据 (忽略 aReadMark 之后的新帧)
4. 释放 WAL_READ_LOCK(k)
Writer 协议:
1. 获取 WAL_WRITE_LOCK (独占锁)
2. 等待所有 active reader 完成 (阻塞或 BUSY)
3. 写入帧到 WAL (追加写)
4. Commit: FSync + 释放 WAL_WRITE_LOCK
Checkpoint 协议:
1. 获取 WAL_CKPT_LOCK (独占锁)
2. FULL/RESTART/TRUNCATE: 额外获取 WAL_WRITE_LOCK
3. 等待所有 active reader 释放
4. 刷 WAL 帧到数据库
5. 更新 salt1/salt2, 释放所有锁
5.3 Reader 锁槽管理
#define WAL_NREADER 5 /* 最大 5 个 reader 锁槽 */
/* 槽分配算法 */
1. 新 reader 尝试:找到 aReadMark 中的最大空闲槽
2. 若所有槽被占用:等待最老 reader 完成
3. aReadMark[0]: 特殊槽,值为 0,表示不使用 WAL,直接读数据库
4. aReadMark[1-4]: 普通 reader 槽,存储 mxFrame 快照
六、Checksum 算法 (亮点)
6.1 算法特点
Fibonacci 反向权重校验和
void walChecksum(u8 *p, int nB, int bigEnd, u32 aCksum[2]) {
u32 s0 = 0, s1 = 0;
u32 *pU32 = (u32*)p;
int n = nB >> 2; /* 32 位整数数量 */
for (int i = 0; i < n; i++) {
u32 x = bigEnd ? sqlite3_getint32(pU32) : sqlite3_getlendo32(pU32);
s0 += x + s1;
s1 += s0;
}
aCksum[0] = s0;
aCksum[1] = s1;
}
权重计算 (反向 Fibonacci):
- x[0] 权重:F(n-1)
- x[1] 权重:F(n-2)
- x[n-1] 权重:F(1) = 1
字节序选择:
0x377f0683大端序 (网络字节序)0x377f0682小端序 (主机字节序)
6.2 验证流程
// 帧有效验证
if (frame.salt1 != header.salt1 || frame.salt2 != header.salt2) {
return INVALID_FRAME; /* 盐值不匹配,废弃 */
}
if (calculatedChecksum != frame.checksum) {
return CORRUPT_FRAME; /* 校验和失败,数据损坏 */
}
return VALID_FRAME;
七、WAL 设计优势与劣势
7.1 优势
| 优势 | 说明 |
|---|---|
| 高并发读取 | Reader 不阻塞 Writer,通过快照实现隔离 |
| 高并发写入 | 通过 Futex/ Mutex 协调,提高写入吞吐量 |
| Atomic Commit | Commit Marker + FSync 保证原子性 |
| Crash Recovery 快速 | 重放有效帧,无需回滚 |
| 空间效率 | WAL 复用,不线性增长 |
| Cost-effective | 无需复杂锁定,仅需 shm 锁 |
7.2 劣势
| 劣势 | 说明 |
|---|---|
| 需要共享内存 | 不支持网络文件系统 |
| Checkpoint 开销大 | 大量帧刷回需要 FSync |
| 内存占用 | 需要维护 wal-index 映射表 |
| 复杂性增加 | 双文件 + 共享内存,调试困难 |
7.3 适用场景
推荐 WAL 模式:
- 高并发读写场景
- 读多写少场景
- 需要快照隔离 (Serializable)
- 需要频繁查询
推荐使用传统 journal 模式:
- 只读数据库 (无并发写入)
- 单用户环境
- 极端内存受限
八、常见问题 (Q&A)
Q1: WAL 与传统 rollback journal 的区别?
A:
| 维度 | WAL 模式 | Rollback Journal 模式 |
|---|---|---|
| 写入方式 | 追加写 (Append-only) | 先写 journal 再写主文件 |
| 并发能力 | 读写可并行 | 串行化 |
| 崩溃恢复 | 重放有效帧 | 回滚 journal 中的修改 |
| 文件大小 | 可变,可重用 | 固定,每事务一帧 |
| 复杂度 | 需要 wal-index 索引 | 相对简单 |
Q2: WAL 如何保证 crash recovery 后的数据一致性?
A: 三层保证机制:
-
Salt-1/2 匹配验证
- 每 checkpoint 变化 salt 值
- 防止旧帧被误认为是新帧
-
Checksum 验证
- 使用 Fibonacci 反向权重算法
- 验证帧的完整性和顺序
-
Commit Marker 标识
- 每 64 帧记录 Commit Marker
- 标识事务边界,确保原子性
恢复流程:
- 读取 WAL Header 和 Frame
- 验证 checksum + salt 一致性
- 重放有效帧到数据库
- 未完成事务的回滚
Q3: Checkpoint 何时触发?
A: 3 种触发方式:
-
显式调用
PRAGMA wal_checkpoint; -
自动触发 (每 1000 帧)
PRAGMA wal_autocheckpoint = 1000; -- 默认值 -
客户端断开时触发
触发条件:
- 显式
PRAGMA wal_checkpoint调用 - WAL 帧数达到
wal_autocheckpoint阈值 (默认 1000 帧) - 最后一个客户端断开连接时
Q4: WAL 的内存锁机制如何工作?
A:
共享内存文件 (.db-shm) 布局
Offset 0-119: 未使用
Offset 120: WAL_WRITE_LOCK (写入锁)
Offset 121: WAL_CKPT_LOCK (Checkpoint 锁)
Offset 122: WAL_RECOVER_LOCK (崩溃恢复锁)
Offset 123-127: WAL_READ_LOCK[5] (读锁槽)
锁实现方式 (平台相关):
- Unix: 通过
futex实现 - Windows: 通过
CreateMutexA+WaitForSingleObject实现 - 跨平台: SQLite 在
os_unix.c/os_win.c中实现
关键点:
- 使用共享内存实现进程间协调
- 每个锁槽 1 字节,简化锁获取逻辑
- 支持最多 5 个并发 reader
Q5: 什么是 WAL 的 Snapshot Isolation?
A: 通过 aReadMark 实现快照隔离:
// Reader 获取快照
WalIndexHdr hdr;
memcpy(&hdr, walIndexHdr(pWal), sizeof(WalIndexHdr));
mxFrame = hdr.mxFrame; /* 记录当前最大帧 */
// Reader 读取页面时
int frameNum;
if (walFindFrame(pWal, pgNo, &frameNum, mxFrame)) {
/* 从 WAL 读取帧 (不超过 mxFrame) */
} else {
/* 从数据库文件读取 */
}
// 保证一致性
/* 只要 reader 坚持使用原始 mxFrame,就不会看到新提交的数据 */
Snapshot 隔离保证:
- 每个 reader 看到一致的数据库视图
- 不会看到其他事务的未提交数据
- 同一时间点的所有读取看到相同数据
Q6: 为什么需要 salt1/salt2 机制?
A: 3 个原因:
- 防止 WAL 复用时的混淆 - 防止旧帧被误认为是新帧
- 防止 Checkpoint 中的竞争 - 确保 new 和 old 帧不会混合
- 增强数据完整性 - 配合 checksum 确保数据一致性
Salt 变化规则:
salt1++ /* 递增,防止旧帧被复用 */
salt2 = random() /* 随机化,防止旧帧被误认 */
验证检查:
if (frame.salt1 != hdr.salt1) return INVALID;
if (frame.salt2 != hdr.salt2) return INVALID;
Q7: 为什么 WAL 不会无限增长?
A: 通过 Checkpoint 机制:
/* WAL 生命周期 */
1. 写入帧到 WAL (增长)
2. Checkpoint 刷入数据库 (复用 WAL 空间)
3. 重置 mxFrame = 0,从新位置继续写入
/* 触发条件 */
- 自动:wal_autocheckpoint 帧数 (默认 1000)
- 手动:PRAGMA wal_checkpoint
- 清理:最后一个读者断开
Q8: WAL 中的 Hash 表如何工作?
A: 快速定位帧的 Page Mapping 机制:
/* WalIndexHashLoc (哈希位置) */
struct WalHashLoc {
u32 iBucket; /* 哈希桶 */
u32 iPage; /* 页面号 */
u32 iFrame; /* 帧位置 */
};
/* 查找算法 */
int sqlite3WalFindFrame(Wal *pWal, Pgno pgNo, int *piFrame) {
/* 1. 计算哈希桶 */
hash = walHash(pgNo, iChange);
/* 2. 查找 frame 位置 */
for (i = 0; i < HASH_NPAGE; i++) {
if (pWal->aPageMapping[hash + i] == pgNo) {
*piFrame = hash + i;
return SQLITE_OK;
}
}
return SQLITE_NOTFOUND;
}
关键特性:
- 4096 个条目 的哈希表
- 1 个 hash 桶,支持最多 4096 帧
- 线性查找,碰撞率较低
九、源码关键函数列表
9.1 生命周期管理
| 函数 | 说明 | 行号 |
|---|---|---|
sqlite3WalOpen() |
打开 WAL 文件 | 1641 |
sqlite3WalClose() |
关闭 WAL 连接 | 2501 |
sqlite3WalLimit() |
设置 WAL 最大大小 | 1710 |
9.2 事务处理
| 函数 | 说明 | 行号 |
|---|---|---|
sqlite3WalBeginWriteTransaction() |
开始写入事务 | 3690 |
sqlite3WalEndWriteTransaction() |
结束写入事务 | 3743 |
sqlite3WalUndo() |
事务回滚 | 3765 |
sqlite3WalSavepoint() |
保存点 | 3830 |
sqlite3WalSavepointUndo() |
回滚到保存点 | 3863 |
9.3 读写操作
| 函数 | 说明 | 行号 |
|---|---|---|
sqlite3WalBeginReadTransaction() |
开始读事务 | 3487 |
sqlite3WalFindFrame() |
查找帧位置 | 3649 |
sqlite3WalReadFrame() |
读取帧数据 | 3667 |
sqlite3WalFrames() |
写入帧到 WAL | 4266 |
9.4 Checkpoint 操作
| 函数 | 说明 | 行号 |
|---|---|---|
sqlite3WalCheckpoint() |
Checkpoint 处理 | 4292 |
sqlite3WalCallback() |
获取回调帧数 | 4430 |
sqlite3WalExclusiveMode() |
设置独占模式 | 4463 |
十、性能优化建议
10.1 参数调优
-- 1. 调整自动 checkpoint 阈值
PRAGMA wal_autocheckpoint = 10000; -- 默认 1000,可根据场景调整
-- 2. 设置 WAL 文件大小限制 (MB)
PRAGMA wal_size_limit = 1024; -- 最大 1GB
-- 3. 调整 checkpoint 模式
PRAGMA wal_checkpoint(TRUNCATE); -- 最积极,立即截断
10.2 设计考量
高频写入场景:
- 调大 wal_autocheckpoint 减少 Checkpoint 频率
- 使用 RESTART 模式 checkpoint
- 增加 wal_max_size 避免频繁截断
高并发读场景:
- 保持默认 autocheckpoint (1000 帧)
- 启用 WAL 共享内存 (支持 5 个 reader)
- 避免使用 TRUNCATE 模式
内存受限场景:
- 使用 smaller wal_page_size (如 2KB)
- 减少 WAL_READER 数量 (最多 5 个)
- 启用 exclusive-mode 减少锁开销
十一、WAL 核心设计思想
11.1 设计哲学
- Write-Ahead: 事务提交前先写日志 (原子性保障)
- Copy-on-Write: 读取时复制数据到 WAL (多版本实现)
- Lightweight: 无需复杂锁定,通过 shm 文件协调
- Efficient Checkpoint: 批量刷回,避免随机写开销
11.2 关键创新
| 创新点 | 作用 |
|---|---|
| Shadow Page | WAL 中不覆盖原数据库,仅追加新帧 |
| Salt Mechanism | 防止旧帧被误认,确保数据一致性 |
| Fibonacci Checksum | 快速检测数据损坏,权重递减算法 |
| Shared Memory Lock | 轻量级锁机制,支持多 reader 并发 |
| Page Mapping Index | 快速定位帧位置,O(1) 平均时间复杂度 |
十二、调试与监控命令
-- 1. 查看当前 WAL 模式
PRAGMA journal_mode; -- 返回 "wal"
-- 2. 查看 WAL 文件状态
PRAGMA wal_checkpoint; -- PASSIVE 模式,输出 checkpoint 结果
-- 3. 强制 Checkpoint 并截断 WAL
PRAGMA wal_checkpoint(TRUNCATE);
-- 4. 查看 WAL 配置
PRAGMA wal_autocheckpoint; -- 显示当前 autocheckpoint 阈值
PRAGMA wal_size_limit; -- 显示当前 WAL 大小限制
-- 5. 生成 WAL 文件信息
PRAGMA integrity_check; -- 检查数据库完整性
PRAGMA wal_checkpoint; -- 检查 WAL 状态
监控指标
- WAL 文件大小 (总帧数)
- Checkpoint 频率
- 锁等待时间 (Writer 等待 Reader)
- 内存使用 (共享内存大小)
- 页面命中率 (从 WAL vs 从数据库)
十三、总结
WAL 的核心价值
- 高并发: Reader/Writer 可并行执行,极大提升并发能力
- 数据一致性: Salt + Checksum 双重保障,确保原子性
- 灵活 checkpoint: 支持 4 种模式,适应不同场景需求
- 轻量级锁机制: 通过 shm 文件实现,无需复杂内核锁
- Crash Recovery 快速: 重放有效帧,无需复杂回滚
附录:常用命令参考
# 1. 编译 SQLite 源码
./configure --enable-wal
make -j$(nproc)
# 2. 运行测试
make test
# 3. 查看 WAL 源码结构
find src/wal*.c -type f | head -10
# 4. 生成 WAL 状态报告
./sqlite3 test.db "PRAGMA journal_mode=wal; PRAGMA wal_checkpoint; PRAGMA integrity_check;"
# 5. 分析 WAL 文件二进制格式
hexdump -C test.db-wal | head -50
# 6. 查看共享内存结构
ls -lh test.db-wal test.db-shm
文档生成时间: 2026-03-30
SQLite 源码版本: 查看 VERSION 文件
源码目录: /Volumes/790/Codes/sqlite/src/wal.c
快速查阅表
| 概念 | 行号范围 | 说明 |
|---|---|---|
| WAL Header 结构 | 1-250 | 文件格式说明 |
| Wal 结构体 | 511-574 | 核心状态结构 |
| Checkpoint 算法 | 4292-4500 | 检查点处理逻辑 |
| FindFrame 算法 | 3631-3700 | 页面帧查找 |
| Hash 表维护 | 1146-1400 | 快速定位机制 |
建议阅读顺序: WAL Header 设计 → 核心结构体 → Checkpoint 算法 → 查找算法 → 源码注释详解