事务管理:Pager 成为 SQLite 中的数据库
Source: Dev.to
Hello, I’m Maneshwar. 我正在开发 FreeDevTools online,目前正在构建 “所有开发工具、技巧代码和 TLDR 的一站式平台” —— 一个免费、开源的中心,开发者可以快速找到并使用工具,而无需在互联网上四处搜索。
到目前为止,我们一直把分页器视为 缓存管理器、状态机 和 磁盘 I/O 的严格守门人。
今天的学习明确了一点:
分页器是 SQLite 中的事务管理器。
锁、日志、缓存状态、保存点——所有这些都在这里协调。
锁管理器可能在操作系统层面执行锁定,但分页器决定 何时、如何、以及 以何种模式 获取和释放这些锁。
SQLite 遵循 严格的两阶段锁定,即使所有数据都是基于文件的,它仍然具备可串行化的行为。和任何严肃的 DBMS 一样,它的事务管理清晰地分为两部分:
- 正常处理
- 恢复处理
在本文中,我们坚定地停留在 正常处理,即一切顺利时的路径。
正常处理:受控的事务流
正常处理是指日常执行时发生的操作:读取页面、修改页面、提交工作、回滚语句以及管理保存点。
分页器(pager)在后台悄悄管理缓存压力的同时,协调完成所有这些工作。
重要提示: 这些逻辑 不在 tree 模块 中。
tree 模块请求页面并修改内存。分页器把这些请求转化为安全、可恢复的操作。
下面我们逐步了解这一流程。
读取操作:进入事务世界
每一次对页面的交互都以相同的方式开始:
sqlite3PagerGet(page_number);
- 这一步是强制性的,即使页面在数据库文件中尚不存在。如果页面超出当前文件大小,分页器会在逻辑上创建它。
- 此调用的首要职责是 加锁。
- 如果当前没有锁(或只有较弱的锁),分页器会尝试在数据库文件上获取 共享锁(shared lock)。
- 如果因为其他事务持有不兼容的锁而无法获取,则读取会以
SQLITE_BUSY失败。
- 若成功获得共享锁,分页器继续执行缓存读取:
- 若页面已经在缓存中,则将其固定(pin)并返回。
- 若不在缓存中,分页器会寻找一个空闲槽位(可能会驱逐其他页面),并从磁盘加载该页面。
此时,tree 模块收到的是指向内存中页面映像的指针,随后是一块 私有空间。这块私有空间在页面第一次进入内存时会被零初始化,随后由 tree 层用于自己的账务管理。
延迟恢复:在现在之前先修复过去
在 第一次获取共享锁 时会产生一个重要的副作用。
当分页器首次在数据库文件上获取共享锁时,它会检查是否存在 热日志文件(hot journal file)。热日志的出现意味着之前出现了错误——如崩溃、断电或在先前事务期间的异常终止。
如果发现热日志,恢复会 立即在此进行:
- 分页器回滚未完成的事务。
- 使用日志恢复页面。
- 完成日志文件的收尾工作。
只有在数据库恢复到一致状态后,sqlite3PagerGet 才会返回给调用者。
结果: SQLite 能保证 即使在崩溃后,你也永远不会从损坏的数据库中读取数据。
读取期间的缓存压力
有时仅仅读取一个页面就会触发写入。
- 如果缓存已满且分页器需要为请求的页面腾出槽位,它必须选择一个受害者页面。
- 若该受害者页面是脏的(dirty),分页器会在复用之前将其刷新到磁盘。
这在正常处理过程中是透明进行的,也是 SQLite 中缓存管理与事务管理不可分割的原因之一。
写入操作:先声明意图再行动
写入是对纪律要求最高的环节。
- 客户端必须已经通过
sqlite3PagerGet将页面固定(pinned)。 - 然后调用:
sqlite3PagerWrite(page);
sqlite3PagerWrite 并不 向磁盘写入任何数据;它仅仅表示“我要写”。
- 在事务中第一次将任意页面设为可写时,分页器会尝试在数据库文件上获取 保留锁(reserved lock)。该锁的含义是:“我计划写入,但尚未实际写”。
- 同一时刻只能有一个分页器持有此锁。如果已有事务持有保留锁或排他锁(exclusive lock),此调用会以
SQLITE_BUSY失败。
成功获取保留锁标志着一次关键的转变:读取事务变为 写入事务。
- 回滚日志被创建并打开。
- 初始日志头部被写入,记录数据库文件的原始大小。
从此以后,分页器必须能够 撤销所有操作。
日志页面:只需一份原始镜像
为了使页面可写,分页器会把该页面的 原始内容 写入回滚日志中……
Source: …
在缓存中的变更与隔离
一旦 sqlite3PagerWrite 返回,树模块就可以自由地修改页面——一次、两次,甚至上百次。分页器 不需要 再次被通知。
更改在内存中累积,受到以下机制的保护:
- 保留锁
- 回滚日志
- 分页器的状态机
仅仅写入并不会触发缓存刷新;磁盘 I/O 被刻意延后。这就是 SQLite 在性能与持久性之间取得平衡的方式。
隔离性与性能——无需复杂的并发控制
在正常处理过程中:
- 页面在 共享锁 下读取
- 写入升级为 保留锁
- 前映像被安全地 记录到日志 中
- 所有修改都保存在 缓存 中
- 数据库文件保持 原始状态
还没有提交任何内容。还没有刷新任何内容。
但用于恢复的一切已经就绪。
接下来
在下一篇文章中,我们将跟踪此事务的自然结局:
- 缓存刷新机制
- 提交操作(以及它们为何分阶段进行)
- 语句级事务和子事务
我与 SQLite 相关的实验和动手执行将放在这里:
lovestaco/sqlite – SQLite examples
参考文献
-
SQLite Database System: Design and Implementation – Sibsankar Haldar (n.d.)
-

👉 查看: FreeDevTools
欢迎提供反馈或贡献!
它是在线的、开源的,随时供任何人使用。
⭐ 在 GitHub 上给它加星: HexmosTech/FreeDevTools