缓存组织:SQLite 实际如何在内存中保存页面
I’m happy to translate the article for you, but I need the full text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line, formatting, markdown, and any code blocks exactly as they are while translating the rest into Simplified Chinese.
Pager和页面缓存概述
到目前为止,我们已经讨论了分页器作为事务管理器和守门人的角色。
现在我们转向页面缓存的物理组织本身——即 SQLite 工作时数据库页面实际所在的地方。这是“页面”不再是抽象概念,而是具有结构、元数据和规则的真实内存块的层面。
- 每个分页器恰好拥有一个页面缓存,由名为 PCache 的处理对象管理。
- 分页器不直接管理缓存内部;它持有对 PCache 对象的引用,并通过一个明确定义的接口与之交互。这种间接方式使 SQLite 能够支持可插拔的缓存实现——如果用户提供自己的缓存模块,SQLite 将使用它。
内置实现位于 pcache1.c,这将是我们学习的对象。
Source: …
PCache 对象
在 PCache 对象内部,一个指针(pCache)指向实际使用的缓存引擎。从分页器的角度来看,缓存是一个黑盒:快速、可预测且可替换。
哈希表槽组织
SQLite 并不把页面存放在一个平面列表中。缓存的页面存放在 槽 中,通过 哈希表 索引:
- 哈希表起始为空。随着页面被请求,槽会被创建并插入。
- 槽的总数受
PCache.nMax限制。 - 默认限制:
- 主数据库和附属数据库 2000 页
- 临时数据库 500 页
- 内存数据库 无限制(仅受可用地址空间限制)
这种设计在保持查找速度的同时,防止内存无限增长。
PgHdr 结构
每个缓存页面由一个 PgHdr 对象表示,仅分页器可见(树模块只能看到原始页面内存)。PgHdr 包含分页器执行正确性检查所需的全部信息:
| 字段 | 含义 |
|---|---|
pgno | 该页面对应的数据库页号 |
dirty | 是否已被修改 |
needSync | 写回之前是否必须先将日志刷新到磁盘 |
nRef | 引用计数(是否被固定) |
pDirtyNext / pDirtyPrev | 脏页链表中的前后链接 |
- 如果
nRef > 0,页面被 固定——正在使用中,不能被回收。 - 如果
nRef == 0,页面是 未固定 的,可以被重新利用。
PCache1 – SQLite的内置缓存
SQLite 的内置缓存(PCache1)增加了另一层。每个哈希表槽位由一个 PgHdr1 对象表示。槽位在内存中的布局大致如下:
[ PgHdr | page image | private space | (optional recovery pointers) ]
- Page image – 原始数据库页。
- Private space – 由树模块用于每页的内存状态。
- Recovery metadata – 为内存数据库存放的恢复元数据。
当页面进入缓存时,整个块会被零初始化,确保不会有陈旧状态在复用时泄漏。
所有槽位都可以通过 PCache1.apHash 数组访问,每个条目指向一个实现为无序单向链表的桶——简单、快速且没有巧妙的技巧。
缓存组模式(可选)
启用 缓存组模式 时:
- 多个
PCache1实例被放入同一个组。 - 一个缓存中的未固定(unpinned)页面可以被另一个缓存回收利用。
- 内存压力以全局方式处理,而不是按缓存单独处理。
当同一进程中存在大量连接且内存紧张时,这非常有用。固定的页面保持私有;只有未固定的槽位会被共享。
内容寻址访问
缓存 不是 你可以索引的数组。客户端永远不知道:
- 页面在内存中的位置。
- 它占用了哪个槽位。
- 它是否最近被驱逐。
页面是通过 页号 请求的,而不是内存地址。当树模块需要页面时,它调用 sqlite3PagerGet(P)——分页器负责其余的工作。
按需获取策略
SQLite 遵循严格的 按需获取 策略:
- 页面管理器 固定(pin)页面并返回它。
- 树模块 临时拥有该页面。
- 客户端最终调用
sqlite3PagerUnref。
在 Unref 之后,页面再次可回收。被固定的页面不能被驱逐。SQLite 还强制执行 最小缓存大小(自 SQLite 3.7.8 起为 10 页),以确保始终有一定的余地进行操作。
页面更改时会发生什么?
接下来合乎逻辑的问题是:页面被修改时会发生什么? 在即将发布的文章中我们将讨论:
- 缓存更新规则
- 脏页的跟踪方式
- 替换策略
- SQLite 如何在性能与安全之间取得平衡
这就是缓存不再是被动的,而开始影响执行的地方。
参考文献与进一步阅读
- SQLite 数据库系统:设计与实现 – Sibsankar Haldar (n.d.)
- 我关于 SQLite 的实验和实践执行
FreeDevTools
👉 查看 FreeDevTools —— 一个面向开发工具、作弊码和 TL;DR 的开源中心。欢迎任何反馈或贡献者!它在线、开源,随时供任何人使用。
⭐ 在 GitHub 上给它加星: