第 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
性能批量写事务包裹;热点写应用层排队
代际每代重开(默认)或全局单例(显式管理)
xrt 延伸阅读