事务管理:Pager 成为 SQLite 中的数据库

发布: (2026年2月4日 GMT+8 02:23)
9 min read
原文: Dev.to

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)。热日志的出现意味着之前出现了错误——如崩溃、断电或在先前事务期间的异常终止。

如果发现热日志,恢复会 立即在此进行

  1. 分页器回滚未完成的事务。
  2. 使用日志恢复页面。
  3. 完成日志文件的收尾工作。

只有在数据库恢复到一致状态后,sqlite3PagerGet 才会返回给调用者。

结果: SQLite 能保证 即使在崩溃后,你也永远不会从损坏的数据库中读取数据

读取期间的缓存压力

有时仅仅读取一个页面就会触发写入。

  • 如果缓存已满且分页器需要为请求的页面腾出槽位,它必须选择一个受害者页面。
  • 若该受害者页面是脏的(dirty),分页器会在复用之前将其刷新到磁盘。

这在正常处理过程中是透明进行的,也是 SQLite 中缓存管理与事务管理不可分割的原因之一。

写入操作:先声明意图再行动

写入是对纪律要求最高的环节。

  1. 客户端必须已经通过 sqlite3PagerGet 将页面固定(pinned)。
  2. 然后调用:
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 Banner
    👉 查看: FreeDevTools

欢迎提供反馈或贡献!

它是在线的、开源的,随时供任何人使用。

在 GitHub 上给它加星: HexmosTech/FreeDevTools

Back to Blog

相关文章

阅读更多 »