第 32 章 / 共 51 章
卷五 · 扩展、运维与实战
SQLite 扩展:持久层的最短路径
272 个符号静态编入宿主,脚本 include 一颗头就能用——demo 的 2004 条数据就是证明。本章把它的 XServer 语境讲全。
本章导读
站位:卷五开卷。SQLite 语义本身看官方文档即可——本章讲三件 XServer 特有的事:变体编入、多 worker 模式、代际句柄决策。完整 API 索引在 参考页。
32.1 引入
服务端应用几乎都要持久化,而单文件部署哲学下"再装一个数据库服务"破坏了整个叙事。SQLite 是天然答案:嵌入式、单文件、零运维——它和 XServer 的组合让"一个 exe + 一个目录"真正覆盖有状态业务。XServer 把它做成可选扩展(不用的部署不背包袱),编译进去的就是原生 API 全集,没有二次包装。
32.2 编入与打开
demo 的打开路径(真实源码)实机验证出处:demo-single/modules/database.h
/* 前置:build.bat sqlite 变体;脚本里 */
#include <sqlite3.h> /* VFS 内,仅 sqlite 变体存在 */
static sqlite3* G_DB; /* 代的状态:每代重开(决策见 32.4)*/
static void DB_Init(void)
{
char sDBFile[512];
snprintf(sDBFile, sizeof(sDBFile), "%s/db/main.db", xsAppPath());
iResult = sqlite3_open_v2(sDBFile, &G_DB,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE /* 读写+不存在则建 */
| SQLITE_OPEN_FULLMUTEX, /* 多 worker 安全 */
NULL);
if ( iResult != SQLITE_OK ) {
printf("[db] open failed: %s\n", sqlite3_errmsg(G_DB));
G_DB = NULL;
return;
}
(void)sqlite3_exec(G_DB,
"CREATE TABLE IF NOT EXISTS item ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL, created INTEGER)",
NULL, NULL, NULL);
}
static void DB_Unit(void) { if ( G_DB ) { sqlite3_close(G_DB); G_DB = NULL; } }
刚才发生了什么:启动日志的 [demo] database opened 就是这段代码;FULLMUTEX 让单句柄可以被 12 个 worker 并发使用;db/ 目录在应用目录里——数据文件天然跨代(ch05 的布局约定在此兑现:换代重开句柄,数据无损)。
32.3 语句五步实战
参数化 CRUD(demo item.h 模式)实机验证出处:demo-single/route_http/item.h(模式节选)
/* 插入:绑定序号从 1 开始;TRANSIENT = SQLite 拷贝参数 */
static bool DB_InsertItem(const char* sName)
{
sqlite3_stmt* p = NULL;
bool bOK = sqlite3_prepare_v2(G_DB,
"INSERT INTO item(name, created) VALUES(?1, ?2)",
-1, &p, NULL) == SQLITE_OK;
if ( bOK ) {
sqlite3_bind_text(p, 1, sName, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(p, 2, (int64)time(NULL));
bOK = sqlite3_step(p) == SQLITE_DONE;
}
sqlite3_finalize(p); /* 每个 prepare 必有 finalize */
return bOK;
}
/* 查询:step 推进一行,column 按列取(列号从 0 开始)*/
while ( sqlite3_step(p) == SQLITE_ROW ) {
int64 iId = sqlite3_column_int64(p, 0);
const unsigned char* sName = sqlite3_column_text(p, 1);
/* ... 装进 xvalue 数组 → ReplyJSON ... */
}
参数绑定(?N 占位)不是风格偏好而是安全边界:字符串拼 SQL 就是注入。demo 全部走绑定——这也是 routebench 能安全灌入 2000 条 bench-write 数据的原因。
32.4 事务与多 worker
- 批量写务必成组:demo 的 bench-write 用事务包裹批量插入——千条写入从"每条一个 fsync"变成"一组一个",差一到两个数量级
- FULLMUTEX 的边界:句柄级互斥保证了"不崩",不保证"快"——热点写路径仍建议应用层排队(单写队列或按业务分库)
- 语句对象不跨线程复用:prepare-step-finalize 尽量在同一次回调内闭环;跨请求复用语句用 reset(不是重新 step 半截语句)
- 代际决策(ch28 的两种姿势):demo 选择"每代重开"——实现零成本且天然安全;全局单例句柄适合"重开很贵 + 重载频繁"的场景,代价是自己管理跨代引用
- 备份:sqlite3_backup_* 在导出面内——在线热备脚本可以直接写
32.5 契约
- 变体线:sqlite 符号只在 sqlite 变体存在;default 下脚本编译失败是预期行为。
- 安全线:一律参数绑定;拼 SQL 等于开门。
- 生命周期线:句柄是代的状态(重开)或显式全局单例——二选一并写进模块注释。
常见错误
错误路径漏 finalize
- 症状
- 长跑后 SQLITE_BUSY / 内存缓慢增长。
- 原因
- prepare 成功但 step 失败的分支直接 return——语句对象没释放。把 finalize 放在统一出口(如上例的单一出口写法)。
绑定与列序号混用
- 症状
- 取到的字段张冠李戴。
- 原因
- bind 从 1 开始、column 从 0 开始——SQLite 的两个差一。对照 SELECT 的列顺序写列号,别靠猜。
编程练习
基础CRUD 四接口
在 demo 基础上给 item 表补全 Update(PUT)路径。
验收:增删改查全链路 curl 通过;重启后数据仍在。
进阶事务吞吐对比
分别用逐条插入与千条事务插入灌数据,对比耗时。
验收:给出量级差结论(预期 ≥ 10×);说明原因(fsync 合并)。
挑战换代数据无损
插入数据 → 热重载(每代重开句柄)→ 验证数据完整并可继续写入。
验收:重载前后 /api/list 的 count 连续递增;无"表不存在"错误。
小结
| 要点 | 结论 |
|---|---|
| 编入 | build.bat sqlite;272 符号;include <sqlite3.h>(VFS) |
| 打开 | open_v2 + READWRITE|CREATE|FULLMUTEX;db/ 目录跨代 |
| 安全 | 参数绑定是硬规则;bind 从 1、column 从 0 |
| 性能 | 批量写事务包裹;热点写应用层排队 |
| 代际 | 每代重开(默认)或全局单例(显式管理) |
本章涉及的 API 参考
xrt 延伸阅读
- xrt 参考 · file_async — 与 SQLite 共存时的异步 IO 习惯