SQLite 中的日志:超越基础
Source: Dev.to
介绍
你好,我是Maneshwar。 我目前正在构建 FreeDevTools online —— 一个免费、开源的中心,汇集所有开发者工具、技巧代码和 TL;DR,集中在一个地方。再也不用在网络上无休止地搜索!
昨天
我们探讨了 回滚日志 —— 前映像、段、头部,以及 SQLite 为什么能够在崩溃后仍然保持数据库文件不被损坏。
今天
我们通过介绍两个不太为人知但至关重要的组件,完成 传统日志 的全貌:
- 语句日志 —— 回滚单个失败的语句。
- 主日志 —— 协调多数据库事务。
它们共同展示了 SQLite 如何保证 正确性,不仅在崩溃时,而且在 语句失败 和 多数据库提交 时也能保持正确。这标志着 存储 + 日志 章节的结束。
明天
我们将提升到更高层次,进入 事务管理与锁机制。
Statement Journal – Rolling Back a Single Statement
一个 statement journal 只为一个非常特定的目的而存在:
撤销在执行过程中失败的单个 SQL 语句的部分影响。
当 INSERT、UPDATE 或 DELETE 触及多行且约束冲突或触发器异常导致语句中止时,就会出现这种情况。外围的 用户事务 必须保持活动状态,而出错的 语句 则需要回滚。这时就需要使用 statement journal。
Statement Journal 是什么(以及不是什麼)
| ✅ 它 是 | ❌ 它 不是 |
|---|---|
| 一个 独立的回滚日志文件 | 不用于崩溃恢复 |
以 临时文件 形式存储(随机名称如 etilqs_*) | 没有段头 |
位于系统 临时目录(例如 /tmp) | 没有校验和 |
| 仅在语句执行期间 存在 | 不会 在崩溃后持久化 |
| 语句结束后 立即删除 | 所有元数据(如 nRec、起始时的数据库大小)仅保存在 内存 中,未写入磁盘 |
Statement Journal 生命周期
关键思想:SQLite 可以在不终止外围用户事务的情况下回滚单个语句。

与 SAVEPOINT 的交互
SAVEPOINT 增加了一层嵌套:
- 为了支持嵌套的 savepoint,SQLite 保留 statement journal,直到相应的 savepoint 被 release,或外层事务 提交。
- 这种设计在提供细粒度回滚语义的同时,保持了 API 对用户的简洁性。
多数据库事务与原子性问题
SQLite 可以在单个连接上附加多个数据库:
ATTACH 'db2.sqlite' AS db2;
现在,一个事务可以修改 main、db2 或任何其他附加的数据库。每个附加的数据库都有它自己的回滚日志并且独立提交。如果没有协调,这将破坏事务的原子性——即“全有或全无”的保证。
主日志 – 协调原子提交
为了在多个数据库之间保持原子性,SQLite 会创建一个主日志,记录事务中涉及的所有单独回滚日志的名称。提交过程如下:
- 写入 主日志(列出所有子日志)。
- 将 主日志刷新到磁盘。
- 提交 每个附加数据库的单独日志。
- 删除 主日志,一旦所有提交成功。
如果任何一步失败,SQLite 可以使用主日志中存储的信息回滚所有数据库,从而确保事务真正是原子的。
结果: 即使有多个附加数据库,事务要么完全成功,要么完全回滚,保持数据完整性。
可视化摘要

TL;DR
| Feature | Purpose | Persistence |
|---|---|---|
| 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 记录。该记录包含:
- 主日志名称的长度。
- 名称的校验和。
- 名称本身(UTF‑8)。
- 一个 禁止页面号(锁字节页)。
这种双向关联保证了恢复过程的正确性。

为什么需要主日志
如果 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 上为项目加星。