恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
【C++三方组件】SQLite:部署最广的嵌入式数据库
首页
资讯中心
/
【C++三方组件】SQLite:部署最广的嵌入式数据库
【C++三方组件】SQLite:部署最广的嵌入式数据库
发布时间:2026/10/12 3:58:57
【C三方组件】SQLite部署最广的嵌入式数据库【摘要】SQLite 把完整的 SQL 数据库引擎装进两个 C 文件零服务、零配置、零管理员数据库就是一个普通文件。本篇讲它的 C API 骨架——open / prepare / bind / step / column / finalize 六步撑起全部日常预编译语句与参数绑定怎么防注入autocommit 与显式事务近千倍的实测差距从哪来WAL 模式下读写的真实并发边界——读不挡写、写挡写最后划出嵌入式与 C/S 数据库的分界线什么时候不该再上 MySQL。【关键词】SQLite、嵌入式数据库、预编译语句、参数绑定、事务、WAL【版本基准】SQLite 3.53.4Public Domainamalgamation 交付示例 C17文中输出与耗时均为本机实测MSVC v145VS2026Windows数字随机器与磁盘而变1. What一个文件里的 SQL 引擎SQLite 是一个嵌入式 SQL 数据库引擎没有服务进程、没有监听端口、没有账号体系整个数据库就是一个普通文件WAL 模式下多两个-wal/-shm附属文件随程序一起部署、随文件一起拷贝。许可 Public Domain——比 MIT 还宽商业闭源、静态链接、改名分发都不受任何约束。交付形态是它最特别的地方官网的amalgamation包把几十万行实现合并成sqlite3.csqlite3.h两个文件直接加进工程编译没有构建系统依赖、没有子模块、没有动态库版本问题。「引入一个数据库」在 SQLite 这里退化成「往工程里加两个文件」。API 面同样窄。日常操作就是一条流水线sqlite3_open 打开不存在则创建数据库文件 sqlite3_prepare_v2 把 SQL 编译成预编译语句 sqlite3_bind_* 绑定 ? 参数 sqlite3_step 执行 / 前进一行 sqlite3_column_* 取当前行的列 sqlite3_finalize 销毁语句必须与 prepare 配对这套 C 接口随 2004 年的 v3.0 定型至今保持稳定C 各种包装SOCI、sqlite_modern_cpp、Qt QSqlDatabase都只是给它穿衣服。本篇直接用原生 API——六个函数撑起全部日常值得看清它们本来的样子。关于「部署最广」SQLite 跑在每一部 Android 与 iOS 手机里系统级组件、每一台桌面浏览器里、无数嵌入式设备里。sqlite.org 的说法是它可能是部署量最大的软件组件之一——这个地位不是靠性能而是靠零部署成本 可靠性换来的第 2 节。存储与并发模型先立两根柱子后面反复用到数据按页默认 4096 字节组织成 B-tree同一时刻只允许一个写者但多个读者可以并存——怎么并存取决于日志模式第 6 节。2. Why自研「把数据存进文件」的账单很多项目都动过这个念头数据结构不复杂自己写个文件存取不就行了真正动手后会发现每一层都有活需求自研要做的事易错点持久化格式定义记录布局、页管理字段加减后旧文件怎么迁移崩溃一致性写日志或影子页fsync 顺序错一步断电即损坏并发访问文件锁、忙等待多线程读写互相踩查询手写遍历与过滤每种取数需求都是一段新代码索引自建 B-tree 或跳表与数据的一致性维护其中崩溃一致性是深水区一次 UPDATE 牵扯数据页、索引页、空闲页三处修改任何一处落盘顺序不对断电后文件就是坏的。SQLite 为此实现了 rollback journal / WAL 两套协议并用一套业界罕见的测试体系兜底——官方 How SQLite Is Tested数千万行测试代码比实现本身多数倍、100% 的 MC/DC 分支覆盖这是航空电子软件 DO-178 的覆盖率标准、每版发布前跑过的测试与 fuzz 总量以亿计。这份可靠性是二十多年攒出来的自研方案最缺的就是它。收益与成本一句话用两个源文件换来 SQL 的声明式取数、事务、崩溃安全与跨平台一致行为——这是三方组件「以小博大」的极致案例。3. How接入与运行两条路按项目习惯选vcpkgvcpkg install sqlite3CMake 走find_package(unofficial-sqlite3 CONFIG REQUIRED)目标是unofficial-sqlite3::sqlite3amalgamation官网下载 zip 解压把sqlite3.c/sqlite3.h编进工程即可——示例工程用BLOG_SQLITE_DIR指向解压目录CMake 里就两行add_library(blog_sqlite3 STATIC .../sqlite3.c)。完整示例 sqlite_demo.cpp一个 main 串起六节演示——绑定插入、查询遍历、autocommit 与事务对比、WAL 吞吐、两连接并发、注入对照。构建与运行入口见 配套说明运行后生成demo.db删掉即可重来。4. 预编译语句prepare 一次、绑定多次4.1 流水线本体sqlite3_stmt*stnullptr;sqlite3_prepare_v2(db,INSERT INTO users(id, name, balance) VALUES(?1, ?2, ?3),-1,st,nullptr);sqlite3_bind_int(st,1,id);// SQLITE_TRANSIENTSQLite 拷走字符串调用方无保活义务sqlite3_bind_text(st,2,name.c_str(),-1,SQLITE_TRANSIENT);sqlite3_bind_int(st,3,balance);sqlite3_step(st);// INSERT 期望返回 SQLITE_DONEsqlite3_finalize(st);三个纪律prepare 与 finalize 必须配对示例里用 RAII 小包装复用语句前先sqlite3_reset绑定会保留、可以只重绑变化的参数字符串绑定想清楚SQLITE_STATIC与SQLITE_TRANSIENT——前者告诉 SQLite「指针活到 step 结束不拷贝」传短命对象用SQLITE_TRANSIENT让它拷一份。查询是同一条流水线的变体sqlite3_step返回SQLITE_ROW表示取到一行sqlite3_column_int64 / column_text按列号从 0 起取值循环到SQLITE_DONE为止完整代码见示例第 2 节。4.2 参数绑定为什么是安全刚需预编译不只是省去重复解析——参数永远是数据不会变成 SQL。实测对照示例第 6 节恶意输入x); DROP TABLE victims; --拼接进 SQL 字符串后交给sqlite3_exec拼接后查询: no such table: victims表已被注入语句删除 绑定后查询: 1 行输入被原样存为数据拼接版为什么惨sqlite3_exec会依次执行分号隔开的每一条语句——注入的DROP TABLE是一条完整合法的 SQL它就真的会跑。这不是 SQLite 特有的坑sqlite3_exec 字符串拼接是 C/C 注入事故的标准路径。规矩只有一条值一律走bindSQL 文本永远是静态的。5. 事务autocommit 的隐含成本SQLite 默认 autocommit每条语句自成一个事务一条 INSERT 背后是完整的 BEGIN → 执行 → COMMIT 流程而 COMMIT 在默认synchronousFULL下要等磁盘真正落盘fsync才返回。于是「循环里逐条写」 每条一趟磁盘往返。实测NVMe SSD各 1000 条 INSERT示例第 3 节autocommit : 9254.2 ms 事务包裹 : 8.8 ms约 1046 倍差三个数量级不是 SQLite 慢是一千次 fsync 和一次 fsync 的差距。把循环包进显式事务n 次落盘合成 1 次exec(db,BEGIN);for(...){/* 绑定 step */}exec(db,COMMIT);这也解释了流传最广的「SQLite 慢」传言多数来自 autocommit 循环写。反过来事务包裹后吞吐完全够看——切到 WAL 模式后 10 万条 INSERT 只要 50 ms约 200 万条/秒本机实测见第 6 节。⚠️ 事务的边界要清楚没 COMMIT 就没持久性——进程崩溃时未提交的事务整体回滚这正是事务的意义sqlite3_close前有未完结的事务会静默回滚。错误恢复的骨架是COMMIT 失败 → ROLLBACK示例的exec返回 false 即走的这条路。6. WAL读不挡写、写挡写6.1 两种日志模式默认的 rollback journal 模式下写事务提交前要拿到独占锁写挡读WALWrite-Ahead Log模式把写改为追加到日志文件读者读的是「日志里最后一次提交的旧快照 主文件」读不挡写读者看旧快照写者继续追加日志互不等待写挡写同一时刻仍然只有一个写者第二个写者拿到SQLITE_BUSY。两连接实测示例第 5 节A 持有未提交的写事务[A] 写事务进行中未提交 [B] 读 count(*) 100000读到旧快照 [B] 写入 - rc5 (database is locked) [A] COMMIT 完成写锁释放 [B] 再次写入 - rc0 (OK)B 的写失败靠sqlite3_busy_timeout(db, 200)兜住——设了它遇到锁会在时限内自动重试超时才报SQLITE_BUSY。不设 busy_timeout 是新手被 “database is locked” 刷屏的第一原因设多长取决于写入事务最长会持锁多久。切换一行PRAGMA journal_modeWAL;。三个使用边界WAL 是数据库的持久属性切一次就写在文件里重开仍在变回 delete 模式需要显式PRAGMA journal_modeDELETE-wal/-shm附属文件与主文件是一体的——备份要一起拷或先 checkpoint正常关闭时 WAL 会合并回主文件WAL 依赖共享内存原语网络文件系统NFS / SMB上不可用——数据库放网络盘的项目只能留在 rollback journal 模式。6.2 一个实测踩到的坑示例开发时真实撞上执行PRAGMA journal_modeWAL取结果的语句 step 之后没有结束未 reset / finalize紧接着的COMMIT报错exec failed: cannot commit transaction - SQL statements in progress未完结的查询语句持有读快照会挡住后续事务提交。教训查询语句用完就 reset / finalize——RAII 配块作用域示例demo_wal_throughput里那对花括号就是为此。这也是长事务挡 WAL checkpoint、-wal文件无限膨胀的同一根因。7. 常用 PRAGMA 速查PRAGMA 是 SQLite 的运行时配置通道没有SELECT语义但读写方式一样。项目开工值得过一遍这几个PRAGMA默认说明busy_timeout0遇锁等待毫秒数几乎总该设foreign_keys关外键约束默认不启用建了外键的库必设 ONjournal_modedeleteWAL 见第 6 节synchronousFULLWAL 常配 NORMAL断电不损坏、最多丢最近的事务page_size4096建库前设置才生效foreign_keys默认关闭是历史包袱也是最反直觉的默认值之一——从其它数据库迁移过来的 schema 若依赖外键兜底务必显式打开。8. 使用边界与选型什么时候不该再上 MySQL / PostgreSQL数据是单机单应用的桌面工具、移动 App、服务端的本地配置与缓存、单文件数据分析——上 C/S 数据库意味着服务进程、端口、账号、网络鉴权和一套备份运维换来的多机共享与权限体系你根本用不上。SQLite 的正确替代品不是 MySQL是「手写文件 结构体数组」。反过来出现这些信号就该离开 SQLite 换 C/S多机访问同一份数据——文件级共享不可行写并发真的高——单写者是硬边界多线程 / 多进程同时写只能排队busy_timeout拉长只会把等待藏起来高频写要靠队列化或换引擎数据量与查询复杂度需要分库分表、执行计划调优、权限分级。嵌入式内部怎么选要 SQL、要关系、要复杂查询选 SQLite本篇纯 KV、写吞吐优先、可接受 LSM 的读放大与调参成本选 RocksDB第 34 篇读多写少、追求极简与稳定延迟选 LMDB第 35 篇。三篇各管一条路线边界在后两篇还会从对向确认。〔关联〕SQLite 的文件格式与 VACUUM、页组织等设计内幕后续在cpp-source-reading专栏展开SQL 事务隔离级别等语言点见cpp-interview-faq。9. 参考资料官方文档 sqlite.org/docs.html推荐通读 Introduction to the C-API本篇流水线的出处与 WALHow SQLite Is Tested可靠性 claims 的原文出处完整构建与运行入口examples/33-34/README下一篇第 34 篇RocksDB——同一「嵌入式存储」赛道上 LSM-tree 路线的写吞吐选手。参考SQLite 3.53.42026-07-24Public Domainamalgamation 交付。本篇全部输出与耗时autocommit / 事务对比、WAL 吞吐、两连接并发、注入对照均为本机实测MSVC v145VS2026WindowsNVMe SSD数字随机器与磁盘而变。