SQLite 中的日志:超越基础

发布: (2026年1月16日 GMT+8 03:38)
9 min read
原文: Dev.to

Source: Dev.to

介绍

你好,我是Maneshwar。 我目前正在构建 FreeDevTools online —— 一个免费、开源的中心,汇集所有开发者工具、技巧代码和 TL;DR,集中在一个地方。再也不用在网络上无休止地搜索!

昨天

我们探讨了 回滚日志 —— 前映像、段、头部,以及 SQLite 为什么能够在崩溃后仍然保持数据库文件不被损坏。

今天

我们通过介绍两个不太为人知但至关重要的组件,完成 传统日志 的全貌:

  1. 语句日志 —— 回滚单个失败的语句。
  2. 主日志 —— 协调多数据库事务。

它们共同展示了 SQLite 如何保证 正确性,不仅在崩溃时,而且在 语句失败多数据库提交 时也能保持正确。这标志着 存储 + 日志 章节的结束。

明天

我们将提升到更高层次,进入 事务管理与锁机制


Statement Journal – Rolling Back a Single Statement

一个 statement journal 只为一个非常特定的目的而存在:

撤销在执行过程中失败的单个 SQL 语句的部分影响。

INSERTUPDATEDELETE 触及多行且约束冲突或触发器异常导致语句中止时,就会出现这种情况。外围的 用户事务 必须保持活动状态,而出错的 语句 则需要回滚。这时就需要使用 statement journal。

Statement Journal 是什么(以及不是什麼)

✅ 它 ❌ 它 不是
一个 独立的回滚日志文件不用于崩溃恢复
临时文件 形式存储(随机名称如 etilqs_*没有段头
位于系统 临时目录(例如 /tmp没有校验和
仅在语句执行期间 存在不会 在崩溃后持久化
语句结束后 立即删除所有元数据(如 nRec、起始时的数据库大小)仅保存在 内存 中,未写入磁盘

Statement Journal 生命周期

关键思想:SQLite 可以在不终止外围用户事务的情况下回滚单个语句。

Statement‑journal lifecycle diagram

与 SAVEPOINT 的交互

SAVEPOINT 增加了一层嵌套:

  • 为了支持嵌套的 savepoint,SQLite 保留 statement journal,直到相应的 savepoint 被 release,或外层事务 提交
  • 这种设计在提供细粒度回滚语义的同时,保持了 API 对用户的简洁性。

多数据库事务与原子性问题

SQLite 可以在单个连接上附加多个数据库:

ATTACH 'db2.sqlite' AS db2;

现在,一个事务可以修改 maindb2 或任何其他附加的数据库。每个附加的数据库都有它自己的回滚日志并且独立提交。如果没有协调,这将破坏事务的原子性——即“全有或全无”的保证。

主日志 – 协调原子提交

为了在多个数据库之间保持原子性,SQLite 会创建一个主日志,记录事务中涉及的所有单独回滚日志的名称。提交过程如下:

  1. 写入 主日志(列出所有子日志)。
  2. 主日志刷新到磁盘。
  3. 提交 每个附加数据库的单独日志。
  4. 删除 主日志,一旦所有提交成功。

如果任何一步失败,SQLite 可以使用主日志中存储的信息回滚所有数据库,从而确保事务真正是原子的。

结果: 即使有多个附加数据库,事务要么完全成功,要么完全回滚,保持数据完整性。

可视化摘要

多数据库事务图

TL;DR

FeaturePurposePersistence
Statement journal回滚 单个 失败的语句,而不中止其所在的事务临时的、内存中的元数据;语句结束后删除
Master journal确保在 多个附加数据库 之间的原子提交持久化,直至整个事务提交或回滚

Source:

使用多个数据库、事务以及 SQLite 内部机制

Athreya(又名 Maneshwar)• 1 月 6 日

标签: #webdev #programming #database #architecture

主日志(Master Journal):协调子日志

为了实现 跨多个数据库的事务全局原子性,SQLite 引入了 主日志(master journal)

重要属性

  • 仅在提交时创建——提交完成后即被删除。
  • 事务中止时永不创建
  • 不包含页面镜像——只存储子回滚日志的文件名。

每个参与事务的回滚日志都会成为 子日志(child journal)

主日志文件

  • 位于 主数据库 同一目录下。
  • 名称格式:-mj(随机后缀用于避免名称冲突)。

主日志的内容

主日志存储 所有子回滚日志的完整 UTF‑8 路径,各路径之间以 NUL(\0)字符分隔。没有页面数据,仅是协调元数据。

子日志如何引用主日志

在提交时,每个子回滚日志会在扇区边界处追加一条 master‑journal 记录。该记录包含:

  1. 主日志名称的长度。
  2. 名称的校验和。
  3. 名称本身(UTF‑8)。
  4. 一个 禁止页面号(锁字节页)。

这种双向关联保证了恢复过程的正确性。

子日志记录布局

为什么需要主日志

如果 SQLite 在提交过程中崩溃,恢复过程如下:

  • 检查每个子日志。
  • 子日志指向主日志,主日志说明 哪些日志属于同一次事务
  • 只有当 所有子日志都已安全提交,事务才被视为完成。

因此,主日志保证了 跨数据库的全局原子性

禁止页面号(锁字节页)

每条 master‑journal 记录都包含一个 禁止页面号,指向 锁字节页(lock‑byte page)。该页面:

  • 被保留且永不写入。
  • 用于处理 Windows 与 POSIX 文件锁语义之间的差异。
  • SQLite 故意不触碰它。

传统日志架构(宏观视图)

日志类型用途
回滚日志(Rollback)崩溃恢复
语句日志(Statement)单语句回滚
主日志(Master)多库原子性

日志架构图

小结

  • SQLite 将 数据和元数据 存放在一个由固定大小 页面 组成的文件中。
  • 第 1 页 锚定数据库并保存全局元数据。
  • 空闲页面通过 空闲列表(freelist) 进行管理。
  • 传统日志使用三种日志类型:回滚日志、语句日志和主日志
    • 回滚日志 确保崩溃安全。
    • 语句日志 确保语句级别的正确性。
    • 主日志 确保跨数据库的全局原子性。

存储和日志的故事到此结束。

接下来:运行时行为——事务类型、锁模式,以及共享锁、预留锁和排他锁之间的区别。我们将从“磁盘上的字节”转向“内存中的并发”。

我的 SQLite 实验和动手示例位于此处:
lovestaco/sqlite – sqlite‑examples

参考文献

  • SQLite Database System: Design and Implementation – Sibsankar Haldar.
  • FreeDevTools – 实用开发工具集合。

👉 查看 FreeDevTools:

欢迎提供反馈或贡献!
⭐ 在 GitHub 上为项目加星。

Back to Blog

相关文章

阅读更多 »

SQLite 内部:文件命名

本章深入到 SQLite 的最低层——在这里,磁盘上的字节变成页面,页面形成树结构,且通过 journaling 来强制实现 durability……

数据库事务泄漏

介绍 我们经常谈论 memory leaks,但在 backend development 中还有另一个沉默的性能杀手:Database Transaction Leaks。我最近…

如何解答 LeetCode 1193

问题描述 表 Transactions 具有以下列: - id 主键 - country - state 枚举:'approved' 或 'declined' - amount - trans...