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;

有效帧验证条件:

  1. salt1、salt2 与 Header 匹配
  2. 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: 三层保证机制:

  1. Salt-1/2 匹配验证

    • 每 checkpoint 变化 salt 值
    • 防止旧帧被误认为是新帧
  2. Checksum 验证

    • 使用 Fibonacci 反向权重算法
    • 验证帧的完整性和顺序
  3. Commit Marker 标识

    • 每 64 帧记录 Commit Marker
    • 标识事务边界,确保原子性

恢复流程:

  1. 读取 WAL Header 和 Frame
  2. 验证 checksum + salt 一致性
  3. 重放有效帧到数据库
  4. 未完成事务的回滚

Q3: Checkpoint 何时触发?

A: 3 种触发方式:

  1. 显式调用

    PRAGMA wal_checkpoint;
    
  2. 自动触发 (每 1000 帧)

    PRAGMA wal_autocheckpoint = 1000;  -- 默认值
    
  3. 客户端断开时触发

触发条件:

  1. 显式 PRAGMA wal_checkpoint 调用
  2. WAL 帧数达到 wal_autocheckpoint 阈值 (默认 1000 帧)
  3. 最后一个客户端断开连接时

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 个原因:

  1. 防止 WAL 复用时的混淆 - 防止旧帧被误认为是新帧
  2. 防止 Checkpoint 中的竞争 - 确保 new 和 old 帧不会混合
  3. 增强数据完整性 - 配合 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 设计哲学

  1. Write-Ahead: 事务提交前先写日志 (原子性保障)
  2. Copy-on-Write: 读取时复制数据到 WAL (多版本实现)
  3. Lightweight: 无需复杂锁定,通过 shm 文件协调
  4. 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 的核心价值

  1. 高并发: Reader/Writer 可并行执行,极大提升并发能力
  2. 数据一致性: Salt + Checksum 双重保障,确保原子性
  3. 灵活 checkpoint: 支持 4 种模式,适应不同场景需求
  4. 轻量级锁机制: 通过 shm 文件实现,无需复杂内核锁
  5. 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 算法 → 查找算法 → 源码注释详解