首页 › programming › 先写日志后改库:SQLite内核拆解

先写日志后改库:SQLite 内核拆解

导语

你手机里几乎肯定躺着几十个 SQLite 数据库——Android 的通讯录、iOS 的备忘录、Chrome 的历史记录、微信的本地缓存,底层全是它。但它没有一个独立的服务进程,不需要安装、配置、调优,甚至整个数据库就是一个文件,可以直接 cp 备份、用 scp 传走、用邮件附件寄出去。The Programming Shuttle 这期《SQLite Internals: How The World's Most Used Database Works》要回答的就是一个看似平淡、实则极难的问题:凭什么一个"玩具般简单"的嵌入式数据库,能成为世界上部署量最大、也被认为最可靠的数据库之一?

答案藏在它四十年工程哲学凝结出的内核里:一套把 SQL 编译成字节码的虚拟机、一棵棵存储一切的 B-tree、一个手握日志与缓存大权的 Pager,以及一层薄到几乎不存在的 VFS。读懂这套内核,你不只是理解了一个数据库,而是理解了"可靠性"这件事在工程上到底是怎么被一行行代码"焊"出来的。

注:受当前网络环境对 YouTube 的访问限制(官方页、字幕接口、yt-dlp、公共前端均返回 bot 拦截),本解读未能逐字引用视频字幕,以下技术展开为基于视频所揭示主题、结合 SQLite 官方架构文档的延伸分析,已尽量标注出处;视频本身提供的是讲解框架与节奏。

视频背景卡片

项目 内容
视频标题 SQLite Internals: How The World's Most Used Database Works
频道 The Programming Shuttle(@theprogrammingshuttle)
时长 深度长视频(架构级讲解,具体以视频页为准)
播放量 持续增长中(实时数据受平台访问限制未取,以视频页为准)
核心主题 SQLite 内部架构、B-tree 存储、Pager 与日志、WAL、可靠性工程

核心内容拆解

一、它凭什么自称"世界最常用的数据库"

先说结论:这个称号不是营销,而是部署量撑起来的硬事实。每一台 Android 手机、每一台 iPhone、每一个主流桌面浏览器、每一架装了某些航电系统的飞机,几乎都在跑 SQLite。作者 D. Richard Hipp 在公开访谈里给出的量级是"万亿级(trillion)活跃数据库实例"——因为一台设备里可能同时跑着成百上千个独立的 .db 文件。

要做到这种渗透率,SQLite 的定位和 MySQL/PostgreSQL 完全不同:它不是"一个跑在服务器上的服务",而是一个被编译进你程序里的 C 库。没有客户端、没有网络协议、没有 DBA、没有端口要开。它的源码以 amalgamation(合并)形式发布——把整个数据库编译进一个 C 文件,编译产物仅数百 KB,直接 #include 进任何工程即可。更关键的是许可证:SQLite 是 Public Domain(公有领域),不是 MIT、不是 Apache——它根本没有版权约束,你可以拿去改、拿去卖、拿去塞进闭源商业产品,连署名都不用。这种"零摩擦"才是它能进每一台设备的真正原因。

延伸:Hipp 在 2000 年为一家做战舰系统的公司(通用动力旗下)写 SQLite,当时的需求就是"一个不需要 DBA、不会挂、能跟着产品走的数据库"。公司与 Hipp 约定放弃版权,SQLite 因此进入公有领域——这段历史直接决定了它今天的普及度。

二、四层架构:SQL 语句到底经历了什么

SQLite 的内核被刻意切成清晰的两半——前端的"编译器"和后端的"虚拟机+存储"。一条 SELECT 从你敲下去到拿到结果,要走完下面这条流水线:

这个分层最大的好处是可移植性:上层逻辑(SQL→字节码→B-tree)完全不依赖操作系统,只有最底层的 VFS 需要针对平台重写。这就是为什么同一个 SQLite 能跑在 Linux 服务器、iOS 手表和飞机的黑匣子里。

三、一个文件,就是一片 B-tree 森林

很多人以为 SQLite 内部是某种"简化版"存储,其实它的存储结构相当正统:整个数据库就是一个文件,文件被切成固定大小的"页"(page,默认 4096 字节,可配 512–65536)。每一张表、每一个索引,都对应一棵独立的 B-tree。

这里有个容易被忽视的设计取舍:与 MySQL/PostgreSQL 的"堆表(heap table)+ 旁挂索引"不同,SQLite 的表本身就是一棵按主键排序的 B+tree——表数据物理上就聚簇在这棵树里。

想绕过这套 B+tree 设计?可以用 WITHOUT ROWID 建表,让表直接按你指定的主键做聚簇 B-tree,省掉一层间接,对某些只按主键查询的场景更快。

四、Pager 才是真正的"灵魂"

如果说 B-tree 决定了"数据怎么摆",那 Pager 决定了"数据怎么活"。B-tree 层从不直接读写磁盘文件——它只跟 Pager 打交道,要一页就调 pager->fetch(pageNo),改完标记成脏页。Pager 在背后默默干着几件性命攸关的事:

  1. 页缓存(page cache):热数据留在内存,避免反复读盘。
  2. 脏页管理:被改过的页先待在缓存里,不到万不得已不落盘。
  3. fsync 的纪律:什么时候、按什么顺序把脏页刷到磁盘,直接决定了崩溃后数据库会不会坏。
  4. 事务边界:一个事务的提交,本质上是 Pager 执行的一套"原子协议"。

理解 SQLite 的可靠性,90% 就是理解 Pager 在崩溃那一瞬间到底做了什么——也就是下面两节的内容。

五、"先写日志,后改库":原子提交的两段式

这是 SQLite 最精妙、也是本期标题真正想点破的地方。在没有 WAL 之前,SQLite 靠的是 rollback journal(回滚日志) 实现原子提交,它用的是一条朴素但强悍的协议——改任何一页之前,先把这一页的原始内容备份到日志文件里:

  1. 获取写锁,开始一个事务。
  2. 要改某一页前,先把这页的原始内容写进回滚日志(journal)文件。
  3. 全部改完准备提交时:先 fsync 日志文件,确保旧值确实落盘了。
  4. 再把修改后的脏页写回主数据库文件,并 fsync。
  5. 最后删除日志文件——这一删,就是"提交点"。

精妙之处在于崩溃恢复的判定极为简单:重启后若发现主库旁边没有 journal 文件 → 上次事务已提交,直接用;若发现 journal 还在 → 上次没提交完,把 journal 里的旧值拷回主库,事务被完整回滚。"日志文件存在与否"这一个事实,就编码了"事务是否完成"这一个布尔值。这是用最小的状态空间实现最大可靠性的经典范例。

代价是性能:每次提交要刷两次盘(日志一次、主库一次),而且写事务期间整个数据库被独占锁锁住——读写互斥。这正是 WAL 要解决的问题。

六、WAL 革命:让读写不再互锁

2010 年的 SQLite 3.7.0 引入了 WAL(Write-Ahead Logging) 模式,思路整个反过来:新数据不再覆盖主库,而是追加写进一个 -wal 文件。读者查询时,同时看主库和 wal,用一份共享内存(-shm,wal-index)快速定位"这一页最新有效版本在哪儿"。结果是:

对大多数读多写少的本地应用,开 WAL 几乎是"零成本提性能":一句 PRAGMA journal_mode=WAL; 搞定。唯一的坑是 WAL 引入了 -wal 和 -shm 两个伴随文件——备份时必须三个一起拷,否则会丢最新未 checkpoint 的写入。

七、为什么它几乎"不会坏"

SQLite 给自己的可靠性目标定得变态高:在任意时刻断电、杀进程、磁盘写一半,数据库都不能进入不可恢复状态。支撑这一点的不是某一行神级代码,而是一整套工程纪律:

这种"把可靠性焊死在每一层"的态度,正是为什么 SQLite 能进飞机、进医疗设备、进你手机里那些"绝对不能丢"的数据。可靠性不是功能,是默认值。

知识结构图

flowchart TD
    A["SQL 文本<br/>SELECT ... FROM ..."] --> B["Tokenizer / Parser<br/>词法 + 语法 → AST"]
    B --> C["Code Generator<br/>AST → 字节码"]
    C --> D["VDBE 虚拟机<br/>基于寄存器,逐条解释执行<br/>可用 EXPLAIN 查看"]
    D --> E["B-tree 层<br/>表=B+tree / 索引=B-tree<br/>按 rowid 排序聚簇"]
    E --> F["Pager 页管理器<br/>页缓存 · 脏页 · 日志 · 崩溃恢复<br/>事务的真正掌权者"]
    F --> G["VFS 操作系统接口<br/>open / read / write / fsync · 锁"]
    G --> H[("磁盘:单个 .db 文件<br/>+ journal / -wal / -shm")]

    F -. "改页前先备份旧值" .-> J["Rollback Journal<br/>原子提交两段式"]
    F -. "新值追加写入" .-> W["WAL 模式<br/>读写不互锁"]

    style D fill:#fef3c7,stroke:#b45309
    style F fill:#dbeafe,stroke:#1d4ed8
    style J fill:#fee2e2,stroke:#b91c1c
    style W fill:#dcfce7,stroke:#15803d

实战启示

1. 把 SQLite 当"应用默认本地存储",而不是 Last Resort。 很多人习惯性地给应用套一个 Postgres/MySQL,其实 90% 的单机应用用 SQLite 更好:零运维、零网络往返(进程内调用是纳秒级)、备份就是拷文件。本地优先(local-first)软件的复兴——Figma 的多端协作、各种笔记/设计工具——背后大量是 SQLite 或它的变体。

2. 上线第一件事:开 WAL。 PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; 这两条配置对绝大多数读多写少的场景是性能与安全的甜点。默认的 DELETE 模式适合只读或极低写入的场景,WAL 才是通用默认。

3. 理解写入的真正代价是 fsync,不是 SQL。 一个事务的耗时几乎等于 fsync 的次数。批量写要合并到一个事务里(BEGIN ... 多条 ... COMMIT),而不是每条自动提交——后者可能慢上几百倍。

4. 并发写的天花板要心里有数。 即使是 WAL,也只允许一个 writer。高并发写入(如多人实时协同、高 QPS 日志写入)是 SQLite 的短板,这时才该上 Postgres/MySQL 或分布式 SQLite 方案(Litestream、Turso/libSQL、dqlite、rqlite)。

5. 备份别只拷主文件。 WAL 模式下 .db/.wal/.shm 三件套必须一起备份;或者更稳的方式是用 .backup 命令在线生成一个一致快照,再拷走。

6. 可靠性可以"抄作业"。 SQLite 用"先写日志再改主数据"实现原子性、用"删除日志=提交点"编码事务状态、用 MC/DC 100% 覆盖保证质量——这些模式完全可以迁移到你自己的关键系统设计里:凡是涉及"改状态又要防崩溃"的地方,先想想"我的 journal 在哪、我的 commit point 在哪"。

延伸学习路径

  1. SQLite 官方架构文档《Architecture of SQLite》(sqlite.org/arch.html)——四层架构的权威出处,是本解读第 2–4 节的骨架。
  2. 《Atomic Commit In SQLite》(sqlite.org/atomiccommit.html)——官方对"两段式原子提交"的逐步推演,读懂它你就真正懂了 rollback journal。
  3. 《Write-Ahead Logging》(sqlite.org/wal.html)——WAL 的官方说明书,含并发模型与 checkpoint 细节。
  4. 《Database File Format》(sqlite.org/fileformat2.html)——想自己解析一个 .db 文件、或理解 B-tree 页布局,看这一篇。
  5. Lex Fridman Podcast #243:D. Richard Hipp——作者亲述 SQLite 诞生史与设计哲学,听完会更懂"为什么是 Public Domain"。

一句话总结

SQLite 把"可靠性"拆解成了四层架构 + 一个"先写日志后改库"的原子协议,让一个几百 KB 的单文件库,成了装在万亿设备上、断电也不坏的工程奇迹——读懂它的 Pager 与日志,你就读懂了所有数据库追求的那件事:在崩溃面前,让数据永远自洽。

参考来源

  1. SQLite Internals: How The World's Most Used Database Works — 本期解读的核心视频(The Programming Shuttle)
  2. Architecture of SQLite — sqlite.org — 四层内核架构官方文档
  3. Atomic Commit In SQLite — sqlite.org — 回滚日志两段式原子提交原理
  4. Write-Ahead Logging — sqlite.org — WAL 模式与并发模型
  5. Database File Format — sqlite.org — 单文件、页与 B-tree 存储格式

    本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

完成摘要(What was done)

下一步计划(What's next)

  1. 若需更高保真,可在网络环境恢复后用 youtube_transcript.py 补抓字幕,对"视频原话 vs 延伸"做逐句校准(优先级:中)。
  2. 视频播放量/时长/发布日三字段当前为占位,可在能访问 YouTube 时回填精确数字(优先级:低,不影响内容实质)。
  3. 如需系列化,下一轮(Round 3=business)可继续沿用同一检索流水线(DDG→oembed)。

注意事项(If applicable)

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入