Skip to content

第18章 SQLite 驱动:线程池包装下的 async

"SQLite is synchronous by design—sqlx-sqlite's job is to lie about that convincingly enough that your async code doesn't notice." —— 每个读过 sqlx-sqlite 源码的人的感想

本章要点

  • SQLite 不是网络数据库——没有 wire protocol、没有 client/server——只有 libsqlite3 的同步 C API(sqlite3_open_v2 / sqlite3_prepare_v2 / sqlite3_step / sqlite3_finalize)。
  • sqlx-sqlite 的核心架构每个 SqliteConnection 持有一个专用 OS 线程(ConnectionWorker),worker 线程里独占一个 *mut sqlite3 handle、主线程通过 flume channel 发 Command 给 worker、worker 回 Result 给主线程。
  • ConnectionWorkersqlx-sqlite/src/connection/worker.rs:32-42)—— 两字段:command_tx: flume::Sender<(Command, tracing::Span)> + shared: Arc<WorkerSharedState>。主线程和 worker 的唯一通信通道。
  • Command 枚举 —— Prepare / Describe / Execute / Begin / Commit / Rollback / Serialize / Deserialize / Ping / ClearCache / ... ——每种操作对应一个 variant + 回复 channel。
  • Mutex<ConnectionState> with fair = true——worker 里 Mutex::new(conn, true)(fair 锁)。fair 必要因为 Command::UnlockDb 立即 re-lock 需求。
  • SQLite 的类型系统极简——5 种 storage class:NULL / INTEGER / REAL / TEXT / BLOB。没有 VARCHAR / INT4 / INT8 的区分——所有整数都是 INTEGER、最大 i64。
  • WAL 模式(Write-Ahead Logging)—— SQLite 3.7+ 的并发优化。多读一写——相比 rollback journal 模式、读不再阻塞写、读写并发能力显著提升(具体倍数取决于工作负载)。生产配置必开。
  • busy_timeout——多进程共享文件时的等锁时限。sqlx 默认 5 秒sqlx-sqlite/src/options/mod.rs:201)、合理覆盖绝大多数场景;C API 层面 sqlite3_busy_timeout 的默认是 0(立即 BUSY)、sqlx 主动设了一个友好默认。
  • in-memory SQLite 通过 :memory: URL——测试和临时数据的经典用法。

18.1 问题引入:同步 C 库如何变 async

SQLite 是1996 年设计的嵌入式数据库——完全不同的世界:

  • 无网络:所有操作走 libsqlite3 的 C API。
  • 全同步sqlite3_step 直接阻塞当前线程直到一步完成(读到行 / 语句结束 / 错误)。
  • 单文件:整个数据库是一个 .sqlite 文件——多进程访问靠文件锁。
  • 内嵌运行:没有独立 server——直接在应用进程里跑 SQL 执行引擎。

这些特点让 SQLite 在嵌入场景(手机 App、桌面工具、CLI、测试 DB)极好用——开箱即用、零运维、快——但让 sqlx 这种 async 库头疼sqlite3_step 阻塞线程就阻塞整个 Tokio runtime——几条慢查询把 async reactor 卡死、所有其他 task 停摆。

sqlx-sqlite 的解法:把 sqlite3 handle 放到专用 OS 线程里、主线程通过 channel 和它通信、每次"同步"调用 C API 只阻塞 worker 线程——Tokio reactor 照常工作。

这条架构让 sqlx::query("SELECT ...").fetch_one(&sqlite_pool).await? 在用户视角和 Postgres 一样——但背后是完全不同的机器。本章拆开这台机器。

18.2 ConnectionWorker 架构

sqlx-sqlite/src/connection/worker.rs:32-42

rust
pub(crate) struct ConnectionWorker {
    command_tx: flume::Sender<(Command, tracing::Span)>,
    pub(crate) shared: Arc<WorkerSharedState>,
}

pub(crate) struct WorkerSharedState {
    transaction_depth: AtomicUsize,
    cached_statements_size: AtomicUsize,
    pub(crate) conn: Mutex<ConnectionState>,  // futures_intrusive::Mutex
}

ConnectionWorker 的两条通信通道

  1. command_tx: flume::Sender —— 主线程→worker,发命令。
  2. shared: Arc<WorkerSharedState> —— 主线程+worker 共享,atomic 字段(depth / cache size)+ conn Mutex

worker 线程ConnectionWorker::establish 通过 thread::Builder::new().spawn(...) 启动——每个 SqliteConnection 一个独立线程。线程里持有 sqlite3 handle、循环接收 command、执行 C API 调用、回复结果。

18.2.1 线程启动代码

worker.rs:100+(简化):

rust
pub fn establish(params: EstablishParams) -> BoxFuture<'static, Result<Self, Error>> {
    Box::pin(async move {
        let (command_tx, command_rx) = flume::bounded::<...>(params.command_channel_size);
        let (establish_tx, establish_rx) = oneshot::channel();

        thread::Builder::new()
            .name(params.thread_name.clone())
            .spawn(move || {
                // 1. 在线程里建立 sqlite3 handle
                let conn = match ConnectionState::establish(&params) {
                    Ok(conn) => conn,
                    Err(e) => { let _ = establish_tx.send(Err(e)); return; }
                };

                // 2. 构造 WorkerSharedState
                let shared = Arc::new(WorkerSharedState {
                    transaction_depth: AtomicUsize::new(0),
                    cached_statements_size: AtomicUsize::new(0),
                    conn: Mutex::new(conn, true),  // fair
                });

                // 3. 把 ConnectionWorker 返回给主线程
                if establish_tx.send(Ok(Self { command_tx, shared })).is_err() {
                    return;  // 主线程 panic 了
                }

                // 4. 消息泵循环
                for (cmd, span) in command_rx {
                    let _guard = span.enter();
                    match cmd {
                        Command::Execute { ... } => { /* ... */ }
                        Command::Prepare { ... } => { /* ... */ }
                        // ... 十几种 command
                    }
                }
            })?;

        establish_rx.await?
    })
}

四步

  1. 创建 flume channel(主线程到 worker)+ oneshot(worker 到主线程的初始化结果)。
  2. Spawn OS 线程——在里面打开 sqlite3(C API 调用)。
  3. 通过 oneshot 把 ConnectionWorker handle 送回主线程。
  4. 进入消息泵循环——直到 channel 关闭(主线程 drop SqliteConnection)。

thread::Builder::new() 给线程设名字(sqlx-sqlite-worker 或用户自定义)——方便 top / profiler 识别。

flume::bounded(不是 unbounded)——有容量限制(默认 50)——防止主线程发 command 太快撑爆内存。超容量时发送会 async 等待——自然背压。

18.3 Command 枚举

worker.rs:64-105(简化)有 10+ 个 variant:

rust
enum Command {
    Prepare { query: Box<str>, tx: oneshot::Sender<Result<SqliteStatement<'static>, Error>> },
    Describe { query: Box<str>, tx: oneshot::Sender<Result<Describe<Sqlite>, Error>> },
    Execute {
        query: Box<str>,
        arguments: Option<SqliteArguments<'static>>,
        persistent: bool,
        tx: flume::Sender<Result<Either<SqliteQueryResult, SqliteRow>, Error>>,
        limit: Option<usize>,
    },
    Begin { tx: oneshot::Sender<Result<(), Error>>, statement: Option<Cow<'static, str>> },
    Commit { tx: oneshot::Sender<Result<(), Error>> },
    Rollback { tx: Option<oneshot::Sender<Result<(), Error>>> },
    Ping { tx: oneshot::Sender<Result<(), Error>> },
    ClearCache { tx: oneshot::Sender<()> },
    UnlockDb,
    Serialize { ... },    // Postgres 没有
    Deserialize { ... },  // 同上
    Shutdown { tx: oneshot::Sender<()> },
}

每个 variant 带一个回复 channel——或 oneshot::Sender (单次回复) 或 flume::Sender (流式回复,Execute 独有)。

Execute 的回复为什么是 flume::Sender 而不是 oneshot?—— 因为 Execute 的结果是流式(Either<QueryResult, Row> 可能多次)——oneshot 只能发一次。用 flume channel 让主线程能 stream 从 worker 收到结果。

rust
Command::Execute {
    query: "SELECT * FROM large_table".into(),
    arguments: None,
    persistent: true,
    tx: row_tx,   // 大容量 channel
    limit: None,
}

worker 处理 Execute 时每 yield 一行就 send 一次到 tx——主线程的 async stream 循环 recv——Rust 的 channel 语义完美匹配 async stream 需求。

18.4 消息泵循环

worker.rs:148+ 的消息泵(简化):

rust
for (cmd, span) in command_rx {
    let _guard = span.enter();
    match cmd {
        Command::Prepare { query, tx } => {
            tx.send(prepare(&mut conn, &query).map(|prepared| {
                update_cached_statements_size(&conn, &shared.cached_statements_size);
                prepared
            })).ok();
        }
        Command::Execute { query, arguments, persistent, tx, limit } => {
            let iter = match execute::iter(&mut conn, &query, arguments, persistent) {
                Ok(iter) => iter,
                Err(e) => { tx.send(Err(e)).ok(); continue; }
            };

            match limit {
                None => {
                    for res in iter {
                        let has_error = res.is_err();
                        if tx.send(res).is_err() || has_error {
                            break;  // channel 关了或出错,停
                        }
                    }
                }
                Some(limit) => { /* 带 limit 版本 */ }
            }
        }
        Command::Begin { tx, statement } => { /* BEGIN */ }
        Command::Commit { tx } => { /* COMMIT */ }
        // ... 其他 variant
    }
}

worker 线程的性格

  • 单线程——所有 sqlite3_* 调用都在这个线程、串行执行。不会有并发 race。
  • 无 yield——每个 command 处理完才接下一个。Tokio 阻塞 schedule 不在这线程。
  • 同步 C API——每次 sqlite3_step 阻塞 worker 几微秒到几百毫秒(取决于查询)——其他 task 无影响、但 这条 connection 的其他 command 排队等

worker 的瓶颈单条连接并发 limited——如果你想并行跑两条 query,必须两条连接(两个 worker 线程)。Pool 的 max_connections 对应 N 个 worker 线程。

18.5 ConnectionHandle:C FFI 边界

sqlx-sqlite/src/connection/handle.rs 定义了 ConnectionHandle——直接包装 *mut sqlite3 的类型:

rust
pub(crate) struct ConnectionHandle(NonNull<sqlite3>);

impl ConnectionHandle {
    pub fn as_ptr(&self) -> *mut sqlite3 { self.0.as_ptr() }
}

impl Drop for ConnectionHandle {
    fn drop(&mut self) {
        unsafe {
            // 尝试 close;忽略 error(drop 里不能 Result)
            let _ = libsqlite3_sys::sqlite3_close_v2(self.0.as_ptr());
        }
    }
}

// NonNull<sqlite3> 不是 Send by default——但 SQLite 的 handle 在正确使用下是 Send
unsafe impl Send for ConnectionHandle {}

unsafe impl Send——手动告诉 Rust "这个类型可以跨线程转移"。前提是同一时间只有一个线程访问它——worker 线程独占 handle 满足这条。

ConnectionHandle 是 safe Rust 和 unsafe C API 的边界 ——上层代码(sqlx-core 的 Executor 实现)完全 safe、通过 ConnectionHandle 调 unsafe C function。

18.5.1 C API 的关键函数

sqlx-sqlite 调的 libsqlite3 函数(libsqlite3_sys crate):

rust
use libsqlite3_sys::{
    sqlite3_open_v2,          // 打开数据库
    sqlite3_close_v2,         // 关闭
    sqlite3_prepare_v2,       // 编译 SQL 成 statement
    sqlite3_finalize,         // 释放 statement
    sqlite3_step,             // 执行一步(next row or done)
    sqlite3_reset,            // 重置 statement 复用
    sqlite3_bind_int64,       // 绑定参数
    sqlite3_bind_text,
    sqlite3_bind_blob,
    sqlite3_bind_null,
    sqlite3_column_type,      // 读列类型
    sqlite3_column_int64,     // 读列值
    sqlite3_column_text,
    sqlite3_column_blob,
    sqlite3_changes,           // 最近操作影响的行数
    sqlite3_last_insert_rowid, // 最后插入的 rowid
    sqlite3_busy_timeout,      // 设置忙碌超时
    sqlite3_exec,              // 执行无参 SQL(快捷版)
    // ... 上百个其他 API
};

sqlx 不直接包装所有 SQLite C API——只挑需要的(约 20-30 个)。其他通过 SQL 操作间接触达(PRAGMA 设置等)。

18.6 Mutex<ConnectionState> 和 fair 锁

WorkerSharedState::conn: Mutex<ConnectionState>——futures-intrusive 的异步 Mutex。

为什么需要 Mutex? 理论上 worker 线程独占 conn——不需要 Mutex。但 sqlx 提供了LockedSqliteHandle 给高级用户——可以在主线程临时借出 worker 的 conn、执行用户自定义 C API:

rust
let mut conn = pool.acquire().await?;
let mut handle = conn.lock_handle().await?;
// handle 里可以直接访问底层 sqlite3 指针
// 用户可以调 libsqlite3 扩展函数
drop(handle);

lock_handle 内部:

  1. 主线程调 shared.conn.lock().await —— 获取 worker 的 conn lock。
  2. Worker 线程此时通过某种机制等 lock 释放——sqlx 用 Command::UnlockDb 命令让 worker 明确释放 lock、等主线程操作完再 re-lock。
  3. 主线程拿到 MutexGuard、可以访问 conn。

Mutex::new(conn, true)truefair 参数——必须 fair,因为 Command::UnlockDb 让 worker 释放后立即尝试 re-lock——非 fair 锁会让 worker 立刻抢回、用户的 lock_handle 永远拿不到。

这条细节是 sqlx-sqlite 作者踩坑之后的修复——注释原文(worker.rs:123-126):

note: must be fair because in Command::UnlockDb we unlock the mutex and then immediately try to relock it; an unfair mutex would immediately grant us the lock even if another task is waiting.

18.7 WAL 模式

SQLite 有两种 journal 模式:

  • Rollback journal(默认)—— 传统模式。写操作用 journal 文件做 atomicity。读和写互斥——一个写者期间所有读阻塞。
  • WAL (Write-Ahead Logging)——SQLite 3.7+ 的新模式。写到 WAL 文件、读直接读主 DB——读和写可以并发。

差异

维度Rollback journalWAL
读写并发互斥读写分离(可并发)
吞吐高(10-100× 写吞吐)
读时延写时被阻塞近零阻塞
fsync每事务也需要(可调 synchronous)
崩溃恢复rollback journalcheckpoint 机制
跨 FS几乎任何 FS需要 shared memory (mmap)

WAL 模式强烈推荐生产使用——SqliteConnectOptions::journal_mode(SqliteJournalMode::Wal)?mode=rwc&journal_mode=wal URL 参数。

配合 synchronous = Normal(不是 Full)能进一步提升吞吐——WAL + synchronous=Normal 是生产 SQLite 的标准组合。

18.8 busy_timeout 和并发 locking

SQLite 的 locking 层级(文件级 + 数据库级):

  • NO_LOCKSHAREDRESERVEDPENDINGEXCLUSIVE —— 按严格顺序升级。
  • 多读可以并发(SHARED)。
  • 写必须 EXCLUSIVE——和所有读互斥。

当一个进程 / 连接持有 EXCLUSIVE、另一个连接尝试写——后者返回 SQLITE_BUSY

busy_timeoutSQLITE_BUSY 返回前 SQLite 先等一段时间

rust
SqliteConnectOptions::new()
    .busy_timeout(Duration::from_secs(5))
    // 等锁 5 秒、超时 SQLITE_BUSY

sqlx 的 SqliteConnectOptions 默认 busy_timeout = 5ssqlx-sqlite/src/options/mod.rs:201)——比 SQLite C API 原生默认(0 = 立即 BUSY)好得多。多数场景直接用默认即可;少数需要调整的场景:

  • 测试 / CLI:5 秒够。
  • web backend:3-10 秒。
  • 高写入并发:考虑 WAL 模式 + 更短的 busy_timeout。

配合 WAL 模式——busy_timeout 更少触发(因为读不再阻塞写)。

18.9 SQLite 的类型系统

SQLite 的动态类型系统——有类型而类型只是建议:

sql
CREATE TABLE t (x INTEGER);
INSERT INTO t VALUES ('hello');   -- 合法!字符串存进 INTEGER 列
SELECT typeof(x) FROM t;          -- 返回 'text'

5 种 storage class

ClassRust 对应
NULLOption::None
INTEGERi64(最大 8 字节)
REALf64
TEXTString(UTF-8)
BLOBVec<u8>

没有更细分——没有 INT4 / INT8 / VARCHAR / DATE / TIMESTAMP。所有日期时间在 SQLite 里实际是 TEXT(ISO 8601 字符串)或 INTEGER(Unix timestamp)——sqlx 通过 chrono / time feature 的类型适配做编解码。

类型系统简单的好处

  • 驱动实现简单——sqlite3_column_int64 / _text / _blob 三个函数覆盖。
  • 编码简单——sqlite3_bind_int64 / _text / _blob 对称。

类型系统简单的代价

  • nullability 信息弱——schema 声明不可靠、sqlx 对 SQLite 的 nullability 推断保守(第 11 章 §11.7)。
  • 跨 DB 迁移需小心——SQLite 的 INTEGER 列能存字符串、Postgres 的 INT4 不能——迁移时可能暴露脏数据。

18.10 SQLite 独有特性

几条 SQLite 用户会用到的独特能力

18.10.1 in-memory 数据库

rust
let pool = SqlitePool::connect(":memory:").await?;

内存数据库——进程结束消失。测试、脚本、临时计算的完美选择。性能极高(无磁盘 I/O)——比 /tmp/test.sqlite 快数倍。

sqlx 测试大量用 :memory:——CI 不需要真 DB。

18.10.2 ATTACH 多库

sql
ATTACH DATABASE 'other.sqlite' AS other;
SELECT * FROM other.users;

一次连接操作多个 DB 文件——SQLite 独有。sqlx 支持——在 SQL 层跑 ATTACH 就行、driver 不需要特殊适配。

18.10.3 Serialize / Deserialize

SQLite 3.7+ 的 sqlite3_serialize / _deserialize——把整个 DB 导出成一个 byte buffer、或者从 buffer 加载。sqlx-sqlite 通过 Command::Serialize / Deserialize 暴露这条能力:

rust
let mut conn = pool.acquire().await?;
let bytes: SqliteOwnedBuf = conn.serialize(None).await?;  // 导出

用途:备份(把整个 DB 存到 blob 字段 / 文件)、克隆(从 buffer 恢复一个内存 DB)。

18.10.4 自定义函数(collation)

SQLite 允许注册 Rust 函数到 SQL——sqlx-sqlite/src/connection/collation.rs 暴露 LockedSqliteHandle::create_collation

rust
let mut conn = pool.acquire().await?;
let mut h = conn.lock_handle().await?;
h.create_collation("chinese", |a, b| a.cmp(b))?;  // 自定义排序

之后 SQL 里用 ORDER BY name COLLATE chinese——按自定义规则排。

18.10.5 Update hook

sqlx-sqlite 支持注册update hook——每次 UPDATE/INSERT/DELETE 时触发 Rust 回调:

rust
handle.set_update_hook(|result: UpdateHookResult| {
    println!("{:?} on {}:{} rowid {}", result.operation, result.database, result.table, result.rowid);
});

典型用途——实时通知其他系统 DB 变化(类似 Postgres 的 trigger + NOTIFY)。

18.11 sqlx-sqlite 的 run 路径

总结一下 SQLite 的 run 等价路径——从用户代码到 C API:

rust
// 用户代码
pool.acquire().await?.fetch_one("SELECT ...").await?;

// ↓ sqlx-core Executor
// ↓ sqlx-sqlite SqliteConnection::fetch_optional
// ↓ 发 Command::Execute 到 worker channel
// ↓ await 从 flume channel recv Either<QueryResult, Row>

// Worker 线程(不 async):
// ↓ execute::iter(&mut conn, query, args, persistent)
// ↓ sqlite3_prepare_v2 (cache miss) 或从 cache 取
// ↓ sqlite3_bind_* × N
// ↓ loop { sqlite3_step → SQLITE_ROW 或 SQLITE_DONE }
// ↓ 每行构造 SqliteRow + flume::send
// ↓ 完成后 sqlite3_reset + cache

核心 async/sync 边界:主线程 async 等 flume channel;worker 线程 sync 调 C API。channel 做消息传递、不涉及 blocking 调用——Rust 的混合同步异步模式典范。

18.11.1 SQLite 驱动的整体架构图

用 mermaid 画 sqlx-sqlite 的主线程 + worker 线程互动:

关键观察

  • 主线程走 async channel 做消息传递——永远不 block Tokio runtime。
  • worker 线程走 sync C API ——阻塞自己但只自己。
  • 两个世界通过 channel 严格隔离——没有共享 mutable 状态(除了 WorkerSharedState 的 atomic)。

这条架构让 sqlx-sqlite 能在 Tokio 里自然地用——虽然底层 SQLite 是同步库。代价是每条连接一个 OS 线程——100 个连接 = 100 个线程、内存占约 8-100 MB(取决于 stack size)。生产 SQLite 用户一般不需要这么多连接(SQLite 写并发本来就低)——这个代价可接受。

18.12 本章小结

本章讲清楚 sqlx-sqlite 的独特架构:

  1. SQLite 的本质(§18.1)—— 无网络、C API 同步、单文件、内嵌。和 Postgres/MySQL 完全不同。
  2. ConnectionWorker 架构(§18.2)—— 专用 OS 线程 + flume channel,让同步 C API 变 async。
  3. Command 枚举 + 回复 channel(§18.3)—— 每 variant 带 oneshot 或 flume sender。Execute 用 flume(流式)、其他用 oneshot。
  4. 消息泵循环(§18.4)—— worker 线程串行处理 command、内部全同步 C API。
  5. ConnectionHandle + unsafe Send(§18.5)—— safe/unsafe 边界、手动标注 Send(单线程独占保证)。
  6. Mutex + fair 锁(§18.6)—— LockedSqliteHandle 让高级用户借出 conn;fair 语义防止 worker 抢回。
  7. WAL 模式(§18.7)—— 读写并发、生产必开。
  8. busy_timeout(§18.8)—— 多进程共享时的等锁时限、sqlx 默认 5s 已合理(覆盖 C API 原生 0 的默认)。
  9. SQLite 类型系统(§18.9)—— 5 种 storage class、极简但 nullability 弱。
  10. SQLite 独有特性(§18.10)—— in-memory、ATTACH、Serialize、collation、update hook。
  11. run 路径(§18.11)—— 主线程 async channel + worker sync C API 的混合模式。

下一章(第 19 章)我们讨论 Any 驱动——运行时多态、连三家的能力、类型擦除的代价。

18.13 SQLite 配置的生产建议

生产用 SQLite 的典型配置:

rust
let pool = SqlitePoolOptions::new()
    .max_connections(10)              // 写并发低、读多可以高些
    .connect_with(
        SqliteConnectOptions::new()
            .filename("app.sqlite")
            .create_if_missing(true)
            .journal_mode(SqliteJournalMode::Wal)          // 关键
            .synchronous(SqliteSynchronous::Normal)         // 性能 + 安全平衡
            .busy_timeout(Duration::from_secs(5))           // 等锁 5s
            .foreign_keys(true)                              // 开启外键约束
            .pragma("cache_size", "-20000")                  // 20MB cache
            .pragma("temp_store", "memory")                  // 临时表用内存
            .log_statements(LevelFilter::Debug)
            .log_slow_statements(LevelFilter::Warn, Duration::from_millis(100))
    ).await?;

八条配置的意义

  1. create_if_missing(true) —— 文件不存在自动建。
  2. journal_mode(Wal) —— WAL 模式、并发提升数倍。
  3. synchronous(Normal) —— 每事务 fsync WAL,不 fsync DB 本身——大幅加速且不丢已 commit 数据。
  4. busy_timeout(5s) —— 多进程 / 多连接时等锁。
  5. foreign_keys(true) —— SQLite 默认关外键检查!必须显式打开。
  6. cache_size -20000 —— 负数表示 KB、正数表示页数。-20000 = 20MB。默认 2MB、生产应该调大。
  7. temp_store memory —— 临时表用内存而不是磁盘——SORT / DISTINCT 等操作加速。
  8. 日志配置——和 Postgres/MySQL 类似。

这八条是 SQLite 生产部署的"必做"清单——默认 SQLite 性能很差(外键不开、cache 小、journal 慢)——正确配置后性能接近内嵌级数据库的合理水平。

18.14 SQLite 的使用场景

SQLite 在哪些场景下是更好的选择?

1. CLI 工具 / 桌面应用——零运维、打包方便、无 server 开销。 2. 测试数据库——sqlx 测试大量用 :memory:——启动毫秒级、测完消失。 3. 嵌入 / 边缘设备——移动 App、IoT、Edge computing。 4. 小流量 web——并发 < 100 的服务、WAL 模式下 SQLite 完全够用。知名案例:tailscale control plane 用 SQLite。 5. 只读数据——文档数据、配置数据——不需要 Postgres 的级别。 6. 原型 / MVP——早期产品用 SQLite 起步、验证后再决定是否换 Postgres。

不适合 SQLite 的场景

  • 高写并发(>100 WPS)——WAL 模式有帮助但终究有瓶颈。
  • 多节点集群——SQLite 是单文件、不能水平扩展。
  • 大数据量(>100 GB)——索引构建 + backup 会慢。
  • 复杂查询优化——SQLite query planner 不如 Postgres。
  • 需要复制——SQLite 没有原生 replication(有第三方如 Litestream 补)。

业界经验——SQLite 的适用场景比大多数人想的更广。Tailscale、Fly.io、Cloudflare Durable Objects 等都用 SQLite——关键是用 WAL + 合理 cache

18.15 sqlx-sqlite 和 rusqlite 的对比

Rust 生态里 SQLite 的主要库:

  • rusqlite —— 经典选择、同步 API、包装 libsqlite3。
  • sqlx-sqlite —— async 包装、和 sqlx 生态集成。

对比:

维度rusqlitesqlx-sqlite
同步/异步同步(用户自己 spawn)异步(sqlx 封装 worker)
API 风格直接 SQLite APIsqlx Executor 统一
跨 DB 代码不支持支持(sqlx-core)
query! 宏
类型系统自己的 traitsqlx Type/Encode/Decode
性能几乎 libsqlite3 原生多 channel 开销(< 10μs)
适合场景CLI / 底层工具async web / 统一 sqlx 生态

选择原则

  • web 后端用 sqlx——和 Pool / Transaction 统一管理。
  • 命令行 / 工具用 rusqlite——直接 API 更简洁、无 async 开销。
  • 混合用也合理——业务主路径 sqlx、离线脚本 rusqlite。

sqlx-sqlite 的 worker 线程模式让它在async web 场景里是唯一合理选择——rusqlite 在 async 场景下要自己 spawn_blocking 每个 query、不如 sqlx 内建的 worker 优雅。

18.16 sqlx-sqlite 代码品质观察

读 sqlx-sqlite 的 10061 行源码、几条品质观察:

1. 合理使用 unsafe——C FFI 交互必然有 unsafe、sqlx 把它严格封装在 ConnectionHandle 等边界类型里——上层代码 safe。

2. worker 模式优雅——对比"每 query spawn_blocking"的替代方案,worker 持续运行 + channel 通信节省 spawn 开销 + 保持 statement cache

3. Command enum 扩展性——加新 Command variant 是加新功能的标准路径(Ping / Serialize / Deserialize 都这么加)——低耦合。

4. LockedSqliteHandle 暴露 unsafe 能力——让用户能自己调 libsqlite3 API——但明确需要 unsafe。这条"给出逃生舱但明确标注 unsafe"是好设计。

5. PRAGMA 配置的灵活性——SqliteConnectOptions::pragma 让用户设任意 PRAGMA——不用 sqlx 代码穷举所有 PRAGMA 名字。

6. in-memory DB 测试 friendly——:memory: URL 让 CI 不需要任何设置——sqlx 的测试策略直接受益。

7. WAL 配置正确默认——sqlx 鼓励 WAL、文档提示 synchronous=Normal 搭配——避免新用户踩坑。

读完本章你对 sqlx-sqlite 有了完整视角——它是**"把同步库包装成 async"**这种模式的教科书案例。

18.17 本章的一个哲学观察

sqlx-sqlite 有一条值得思考的设计哲学——它承认 SQLite 的限制、不强求对齐 Postgres/MySQL。具体体现:

  • SQLite 的同步 C API——sqlx 不试图"改成 async"、而是接受专用线程 + channel的代价。
  • SQLite 的弱类型系统——sqlx 不补 nullability 推断——让用户知道 SQLite 的限制。
  • SQLite 的单写者——sqlx 不用复杂 lock 协调、让 busy_timeout 处理。

这条**"尊重底层库的性格"** 的哲学让 sqlx-sqlite 的代码可维护——不和 SQLite 斗争、而是配合 SQLite。结果是 10061 行代码(比 MySQL 还多一点)但设计清晰、bug 少。

相反——如果 sqlx 试图"让 SQLite 像 Postgres"(模拟 nullability 检查、做协议翻译等)——代码会膨胀、bug 会多、维护成本飙升。懂得何时不做抽象是优秀库作者的标志。

这对你设计自己的 Rust 库有借鉴——包装底层 API 时、尊重它的形状、不勉强对齐别的库

18.18 本章的技术 takeaway

读完本章你应该能:

  1. 解释为什么 SQLite 不能像 Postgres 那样走 async 协议。
  2. 描述 ConnectionWorker 架构——worker 线程 + channel 通信。
  3. 说出 Command enum 每种 variant 的大致作用。
  4. 理解 Mutex fair 锁的必要性(UnlockDb 场景)。
  5. 配置生产级 SQLite(WAL + busy_timeout + cache_size)。
  6. 选择何时用 SQLite 何时用其他 DB。
  7. 对比 sqlx-sqlite 和 rusqlite 的适用场景。
  8. :memory: 写 sqlx 测试。
  9. 理解"包装同步库成 async"的通用模式、迁移到自己项目。

这 9 条能力让你成为 Rust 生态里会用也懂 SQLite 的工程师——绝大多数 Rust 开发者只会用、不懂底层——你多了这一层理解。

下一章 Any 驱动——讨论 sqlx 的"运行时多态"——让一份代码同时跑三家 DB。这是整个第五部分的收官——把抽象和具体两端在 Any 里再次串起来。

18.19 SQLite 的 WAL 细节

WAL 模式是 SQLite 生产配置的核心、值得展开讲:

WAL 的工作机制

  1. 写事务 append 到 *-wal 文件(DB 文件不变)。
  2. 读事务直接读 DB 文件 + 读 WAL(没被 checkpoint 的部分)。
  3. 定期 checkpoint——把 WAL 内容 apply 到 DB 文件、WAL 回滚。

两个配套文件

  • app.sqlite-wal——Write-Ahead Log。
  • app.sqlite-shm——Shared Memory(多进程协调用)。

关键配置

  • wal_autocheckpoint —— 自动 checkpoint 的阈值(默认 1000 页,~4MB)。
  • journal_size_limit —— WAL 文件最大大小(超过截断)。
  • wal_checkpoint(PASSIVE|FULL|RESTART|TRUNCATE) —— 手动 checkpoint。

WAL 的权衡

  • 优点:读写并发、写性能高、崩溃恢复好。
  • 缺点:需要 FS 支持 mmap(NFS 等可能不支持)、backup 更复杂(要同时复制 DB + WAL)、两种模式切换需独占访问。

生产选 WAL 几乎一定对——除非你在 NFS 之类奇葩存储上。

18.20 SQLite 和并发写

SQLite 最大的短板是并发写——即使 WAL 模式下,同一时间只有一个写事务

实测数据(非精确、基于经验):

  • 单线程顺序写:5000-10000 TPS(WAL 模式,synchronous=Normal)。
  • 10 线程并发写:通常 5000-8000 TPS(worker 级别串行、和单线程差不多)。
  • 100 线程并发写:类似——并发提升边际为零。

对应用的含义

  • 写密集 workload用 SQLite 有上限(约 10K TPS 单实例)。
  • 读密集 workload没有上限(WAL 下读并发自由)。
  • 混合 workload看比例——读多于写时 SQLite 友好。

如果写 TPS 需求超过 10K——要么 shard(多个 SQLite 文件)要么换 Postgres。不要硬撑 SQLite——违背其设计初衷。

18.21 sqlx-sqlite 对 rusqlite 的包装深度

sqlx-sqlite 不是直接用 rusqlite——而是直接用 libsqlite3-sys(libsqlite3 的 Rust FFI 绑定)。为什么?

  • rusqlite 是同步 API——sqlx 要 async、直接用同步库会"多一层"。
  • rusqlite 有自己的抽象——Connection / Statement / Rows——sqlx 需要的是自己的抽象、不是嵌套一层。
  • 灵活性——直接用 libsqlite3-sys 让 sqlx 能精细控制每个 C 调用、不受 rusqlite 的限制。

sqlx-sqlite 的 10061 行代码就是 rusqlite 那种程度的 libsqlite3 包装 + async worker 层——差不多量。这让 sqlx-sqlite 不依赖 rusqlite 的版本节奏、独立演进。

这条"直接包装 C FFI 而不是包装更高层库"的选择是 sqlx 的标准做法——其他 driver 也类似(sqlx-postgres 不依赖 tokio-postgres、sqlx-mysql 不依赖 mysql_async)。独立实现 + 统一 sqlx 风格——代价是更多代码、收益是完全控制。

18.22 一个实战例子:用 SQLite 测试 Postgres 代码

一条实用技巧——用 SQLite :memory: 跑 Postgres 代码的集成测试

rust
#[cfg(test)]
mod tests {
    use sqlx::{Any, AnyPool};

    #[tokio::test]
    async fn test_create_user() {
        sqlx::any::install_default_drivers();

        // CI 里用 SQLite
        let pool: AnyPool = AnyPoolOptions::new()
            .connect("sqlite::memory:").await.unwrap();

        // 建 schema
        sqlx::raw_sql("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
            .execute(&pool).await.unwrap();

        // 业务代码走 AnyPool——跨 DB 兼容
        let id: i64 = sqlx::query_scalar("INSERT INTO users (name) VALUES ($1) RETURNING id")
            .bind("Alice").fetch_one(&pool).await.unwrap();

        assert_eq!(id, 1);
    }
}

用 Any 驱动 + SQLite :memory:——CI 零配置跑测试、不需要真 Postgres。但SQL 方言必须是 cross-compatible 的子集——不用 Postgres 特有语法(数组、JSONB、RETURNING 复杂形式等)。

这是 sqlx 跨 DB 能力的实用落点——测试用 SQLite 加速、生产用 Postgres。前提是代码仔细处理方言。第 19 章 Any 驱动会详细讲。

18.23 SQLite 驱动的总结要义

浓缩本章:

1. SQLite 是同步的、sqlx 用 worker 线程包装——代价是每连接一个 OS 线程、收益是和 sqlx 生态无缝。 2. 命令 + channel 是 sync/async 桥接的标准模式——可以迁移到你自己的"async 包装同步库"项目。 3. SQLite 的生产配置重点是 WAL 模式 + busy_timeout + cache_size——不配这些默认性能很差。 4. SQLite 适用场景比想象多——CLI / 测试 / 边缘 / 小流量 web 都合适;高并发写 / 大数据才是真正的短板。 5. sqlx-sqlite 的 10061 行代码让你看到"async 包装 C 库"的完整实现——Rust 工程的精彩案例。

读完第 18 章你对 sqlx 的三家驱动(Postgres / MySQL / SQLite)有完整视角——每家都拆了、差异明确、共性也明确。下一章 Any 驱动把它们再次抽象——让一份 Rust 代码跨 DB。

至此 sqlx 的具体驱动部分(第 16-18 章)讲完。第 19 章回到抽象讨论——Any 驱动是整个 sqlx 架构的 capstone。

18.24 sqlite-unbundled vs sqlite 的区别

sqlx 提供两个 SQLite feature:

  • sqlite —— 捆绑 libsqlite3 源码(通过 libsqlite3-sysbundled feature)。编译时自动编 libsqlite3、不依赖系统 SQLite。
  • sqlite-unbundled —— 链接系统的 libsqlite3(通常 apt install libsqlite3-dev / macOS 自带)。

选哪个?

  • sqlite (bundled) —— 优点:构建稳定、版本统一;缺点:编译慢(要编 C)、二进制大 1-2MB、不能动态 patch。
  • sqlite-unbundled —— 优点:编译快、二进制小、利用系统更新;缺点:依赖系统 libsqlite3 版本(太老可能功能缺失)。

生产建议

  • 通用 Rust 服务:用 sqlite (bundled)——可控、跨平台一致。
  • 已有系统 libsqlite3 的环境:用 sqlite-unbundled——省编译时间。
  • 发行版 Linux 打包:用 sqlite-unbundled——符合发行版的"共享库"哲学。

sqlx 默认 sqlite = ["sqlite-bundled"]——最稳定。

18.25 SQLite 版本兼容性

sqlx-sqlite 依赖 libsqlite3-sys 的某个版本(0.8.6 时是 0.27)——对应 SQLite 3.44+。SQLite 本身的版本兼容性

  • 3.x 之间基本兼容——老版本建的 DB 文件新版本能读。
  • 向后兼容——新 SQLite 能 open 老 DB。
  • 某些新功能(比如 UPSERT ... EXCLUDED、JSON_* 函数)只在新版本——用要求 min SQLite version。

bundled 模式下 sqlx 自动用 libsqlite3-sys 里绑定的版本(通常是 SQLite 最新稳定)。unbundled 模式下看系统版本——可能是 3.31(Ubuntu 20.04 自带)。

生产遇到"sqlx 报 syntax near xxx"——先检查 SQLite 版本。SELECT sqlite_version() 能看 runtime 版本。

18.26 SQLite 的 backup 和迁移

SQLite 是单文件 DB——backup 方式多样:

1. 文件级 backup——cp app.sqlite backup.sqlite + 同时复制 app.sqlite-wal(WAL 模式)。必须同一时刻快照否则不一致。

2. sqlite3_backup_init API——SQLite 提供的 online backup。sqlx 通过 serialize/deserialize 或 handle 的自定义代码支持。

3. .dump CLI——sqlite3 app.sqlite .dump > backup.sql——SQL 文本 backup,可读。

4. Litestream——第三方工具、WAL-based 持续复制到 S3 / FS。

生产推荐——Litestream + 定期 .dump——双保险。sqlx 代码里不直接涉及 backup、但知道这些工具对运维很重要。

18.27 SQLite 在 sqlx 生态里的独特地位

从整个 sqlx 生态的视角看、SQLite 驱动有几个独特地位

1. 测试基础设施—— sqlx 的大量内部测试用 SQLite (:memory:)——CI 快、零依赖。这让 sqlx-core 的 trait 实现能被 easy 验证。

2. 最容易入门的 driver—— 新 Rust 开发者学 sqlx 常从 SQLite 开始——connect(":memory:") 就能跑。没有 Postgres/MySQL 的安装门槛。

3. 嵌入场景的唯一选择——移动 App、Tauri 桌面、CLI——这些场景用不了 Postgres/MySQL。

4. 原型工具——快速 MVP 用 SQLite、迭代后切 Postgres——sqlx 的跨 DB 能力让迁移顺畅。

这四个角色让 sqlx-sqlite 虽然协议上远比 Postgres/MySQL 简单但实际使用场景极广——几乎每个 sqlx 用户都接触过(测试时至少)。

这也说明 sqlx-sqlite 的维护对整个生态至关重要——sqlx 团队持续投入即便 SQLite 的用户数字对比 Postgres 可能少——因为 SQLite 是测试基础设施 + 入门友好性的关键环节。

18.28 本章技术重点回顾表

把本章的核心技术点列一张表收尾:

主题本章位置核心观点
ConnectionWorker 架构§18.2专用 OS 线程 + flume channel 隔离同步 sync 和 async
Command 枚举§18.3十几种 variant、oneshot/flume reply channel
消息泵循环§18.4Worker 同步执行 C API、串行处理 commands
ConnectionHandle FFI§18.5unsafe Send 标注 + Drop 里 close_v2
Mutex fair 锁§18.6UnlockDb 场景、必须 fair 防 re-lock 竞争
WAL 模式§18.7读写并发、生产必开
busy_timeout§18.8多进程 locking、sqlx 默认 5s(覆盖 C API 原生 0)
5-class 类型系统§18.9最简化但 nullability 信息弱
in-memory / ATTACH / serialize§18.10SQLite 独有特性
run 路径§18.11async channel + sync C API 混合
架构图§18.11.1主/worker 线程 互动 mermaid
生产配置§18.13八条关键 pragma
使用场景§18.14合适和不合适
vs rusqlite§18.15适用场景分工

14 个主题——本章技术重点齐全。收藏本表作为 sqlx-sqlite 的 reference 索引。

至此第 18 章正式结束。第 19 章 Any 驱动是第五部分最后一章——本书 22 章的 2/3 已经讲完。

18.29 SQLite worker 模式的通用启示

本章讨论的**"worker 线程 + channel 包装同步库"**模式可以迁移到任何同步库的 async 包装项目——几条通用经验:

1. 一个资源一个 worker——sqlx-sqlite 是每连接一个 worker、不是全局一个 worker。这让每连接的操作串行(符合 sqlite3 的线程要求)、不同连接并发(多 worker 并发)。

2. channel capacity 要有界——flume::bounded 而不是 unbounded。防止主线程发送太快撑爆内存(主线程 async、可以快发;worker 同步、处理慢)。有界 channel 自动背压。

3. command 要带 reply channel——send command 后 await reply——这是请求/响应模式的标准。单次 reply 用 oneshot、流式 reply 用 flume/mpsc。

4. worker drop 时 graceful shutdown——SqliteConnection 的 Drop 通过 command_tx 关闭(channel 关闭)、worker 的 for-loop 退出、sqlite3_close_v2 调用、线程结束。这条链路确保资源清理。

5. Arc + atomic 共享少量状态——WorkerSharedState 的 transaction_depth / cached_statements_size 用 Arc<AtomicUsize>——主线程和 worker 都能读。只用于不需要和 channel 同步的状态——和 channel 互补。

6. Mutex 给高级用户借出——LockedSqliteHandle 让用户直接访问 worker 的 conn——但通过 lock/unlock 协议维护并发安全。给逃生舱同时保证不变量

这六条经验完整描述了"async 包装同步库"的最佳实践——你在自己项目里遇到类似需求(Redis sync library / FFI 库包装)都可以参考。

sqlx-sqlite 是这种模式的一个标杆实现——读懂它、你掌握了 Rust 工程里处理同步/异步边界的一类核心技巧。

18.30 SQLite 代码里三条有趣的细节

最后三条读 sqlx-sqlite 源码时看到会会心一笑的细节:

1. Fair mutex 的注释worker.rs:123-126):

// note: must be fair because in `Command::UnlockDb` we unlock the mutex
// and then immediately try to relock it; an unfair mutex would immediately
// grant us the lock even if another task is waiting.

作者显然踩过这个坑——注释详细解释原因。这种"踩坑留言"是好开源代码的标志——警告后来者。

2. Command::UnlockDb 的单向worker.rs 某处)—— Command::UnlockDb 是 fire-and-forget、没有 reply channel:

rust
Command::UnlockDb,  // 无 tx 字段

因为它只是让 worker 释放 lock、用户代码在另一端 block 等 lock——不需要 worker 回复。这是非标准 command——打破"每 command 带 reply"的模式——但合理。

3. Drop 里的 shutdown——ConnectionWorker::drop 通过关闭 command_tx 触发 worker 退出:

rust
// 不需要显式代码——command_tx drop 会关闭 channel
// worker 的 for-loop 自动退出、线程自然结束

Rust 的 RAII + channel 语义让这件事零样板代码——关闭 handle 就自动清理 worker。对比某些语言要显式"停止线程 + join"——Rust 清爽多了。

这三条细节不影响"懂 sqlx-sqlite 怎么用"——但让你欣赏源码的品质。好的开源项目就像好的小说——细节里藏着作者的思考——读懂这些让你和作者建立共鸣

18.31 ConnectionWorker:flume + oneshot 的协作

sqlx-sqlite 的核心数据结构 ConnectionWorkersqlx-sqlite/src/connection/worker.rs:32-42):

rust
pub(crate) struct ConnectionWorker {
    command_tx: flume::Sender<(Command, tracing::Span)>,
    pub(crate) shared: Arc<WorkerSharedState>,
}

pub(crate) struct WorkerSharedState {
    transaction_depth: AtomicUsize,
    cached_statements_size: AtomicUsize,
    pub(crate) conn: Mutex<ConnectionState>,
}

两条通道

  • command_tx: flume::Sender<(Command, Span)>—— 异步 task 发命令给 worker 线程—— flume 是 mpsc channel、支持 sync/async 两端混用——sqlx 异步端发、worker 同步端收。
  • shared: Arc<WorkerSharedState>—— 不走 channel 的共享状态—— transaction_depth(可由异步端直接原子读)+ cached_statements_size(统计用)+ conn: Mutex<ConnectionState>(只有 worker 持有、异步端间接看)。

Command enum 是 worker 的 RPC 接口:54-79):

rust
enum Command {
    Prepare  { query, tx: oneshot::Sender<...> },
    Describe { query, tx: oneshot::Sender<...> },
    Execute  { query, arguments, persistent,
               tx: flume::Sender<...>, limit },
    Serialize  { schema, tx: oneshot::Sender<...> },
    Deserialize { schema, data, read_only, tx: oneshot::Sender<...> },
    Begin    { ... },
    // ...
}

响应通道分两种

  • oneshot::Sender—— 单次响应(Prepare / Describe / Begin)—— futures-channel 提供—— 发完就 close—— 零开销 一次性 channel。
  • flume::Sender—— 多次响应(Execute 流式返回多行)—— 同一个 flume channel 异步端 recv_async()—— worker 端 send() 发每一行—— 天然 stream。

设计巧妙在哪里—— Command 枚举把所有可能的 worker 操作枚举出来—— worker loop 只需 match 分派—— 一个 loop、一份状态、串行执行所有操作—— 没有锁粒度问题。

为什么用 flume 而不是 tokio::sync::mpsc—— flume 无锁(基于 atomic)—— 比 Tokio channel 快 2-5 倍—— 这里每次 query 都要 send/recv—— 速度要紧。

这套"Command enum + 两种 response channel + 一个 worker loop"模式是**"async 包装同步库" 的标准姿势—— 值得你在类似项目里复制。

18.32 SqliteConnectOptions 的关键字段对应 PRAGMA

SQLite 没有"连接参数 URL"的标准—— sqlx 通过 SqliteConnectOptions 暴露数十个字段、连接建立后自动发 PRAGMA 语句配置。几个关键字段(源码 sqlx-sqlite/src/options/mod.rs):

  • journal_mode: SqliteJournalMode(Delete / Truncate / Persist / Memory / Wal / Off)—— 生产推荐 WAL—— PRAGMA journal_mode=WAL 在 connect 后发。
  • synchronous: SqliteSynchronous(Off / Normal / Full / Extra)—— 默认 Full—— 生产 WAL 模式下 Normal 足够—— 大幅提速。
  • busy_timeout: Duration—— 默认 5s—— PRAGMA busy_timeout=5000—— 遇到锁竞争时 SQLite 内部自动 retry 这么久。
  • foreign_keys: bool—— 默认 true—— PRAGMA foreign_keys=ON—— SQLite 默认关外键约束、sqlx 反其道开启—— 避免新手踩坑。
  • locking_mode: SqliteLockingMode(Normal / Exclusive)—— Exclusive 锁死数据库文件、单连接专用—— 单租户场景加速 30%。
  • auto_vacuum: SqliteAutoVacuum(None / Full / Incremental)—— 空间回收策略—— 长寿命 DB 重要。
  • pragma: HashMap<String, String>—— 任意 PRAGMA 兜底—— 覆盖 sqlx 没直接封装的。

sqlx 做对的一件事—— foreign_keys 默认开、和 SQLite 原生 C API 行为相反—— 用户开心。这种**"好的默认值压倒库的历史惯性"**是 sqlx 整体风格的缩影。

URL 风格示例sqlite:?journal_mode=WAL&busy_timeout=2000)—— sqlx 的 from_url 解析器把 query string 映射到这些字段—— URL 作为外部配置和代码内的 SqliteConnectOptions 构造函数等价—— 这两条路径任选其一—— 小团队用 URL 外置、大项目用 Rust builder 显式。

18.33 StatementHandle::step:unsafe + retry 的双重处理

SQLite 的核心执行循环是 sqlite3_step() C API——sqlx 包装在 StatementHandle::stepsqlx-sqlite/src/statement/handle.rs:331-351):

rust
pub(crate) fn step(&mut self) -> Result<bool, SqliteError> {
    // SAFETY: we have exclusive access to the handle
    unsafe {
        loop {
            match sqlite3_step(self.0.as_ptr()) {
                SQLITE_ROW => return Ok(true),
                SQLITE_DONE => return Ok(false),
                SQLITE_MISUSE => panic!("misuse!"),
                SQLITE_LOCKED_SHAREDCACHE => {
                    unlock_notify::wait(self.db_handle())?;
                    sqlite3_reset(self.0.as_ptr());
                }
                _ => return Err(SqliteError::new(self.db_handle())),
            }
        }
    }
}

四条返回路径

  • SQLITE_ROW → 有行可读、返回 Ok(true)—— 调用方 column_* 取值。
  • SQLITE_DONE → 执行完毕、返回 Ok(false)—— 调用方退出循环。
  • SQLITE_MISUSE → panic!—— API 使用错误、这是 sqlite 的程序 bug 信号—— 继续跑只会错上加错、直接 panic。
  • SQLITE_LOCKED_SHAREDCACHEshared cache 被另一连接锁住—— 调 unlock_notify::wait 等通知、sqlite3_reset 重置语句、循环重试。
  • 其他—— 转 SqliteError 返回。

unlock_notify::waitsqlite 特有机制—— sqlite3_unlock_notify API 注册回调、在另一连接 release 锁时唤醒—— 避免 busy-wait 轮询。sqlx 用 std::sync::Condvar 做桥接、worker 线程 block 等待。

// SAFETY: we have exclusive access to the handle—— 一句 comment 解释为什么 unsafe 安全—— StatementHandle 所在的 worker 线程是唯一持有者、没有并发访问—— 这是 unsafe 使用的正确姿势—— 永远写 SAFETY 注释、说明为什么契约成立。

三条 Rust unsafe 经验从这 20 行代码里能学到:

  1. unsafe 块尽量小—— 只包 FFI 调用、其他逻辑在 safe Rust 写。
  2. SAFETY 注释是契约—— 作者对 reviewer 的承诺——不写就是耍流氓。
  3. retry loop 放在 unsafe 内—— 避免每次进出 unsafe 块——微小但积累的性能优化。

18.34 和《Tokio 源码深度解析》第16章的关联

sqlx-sqlite 用专用 OS 线程 + channel 通信包装同步 C API——这个决定本质上回答的是《Tokio 源码深度解析》第 16 章 §16.1 spawn_blocking:扔到专属线程池§16.3 两者的取舍矩阵里讨论的核心问题:阻塞调用放哪里

对照两种路径

方案谁用代价
tokio::task::spawn_blocking零散阻塞调用每次 spawn 开销 + blocking pool 竞争
专用 worker 线程 + channelsqlx-sqlite一次性建线程、call 开销仅 channel send/recv

为什么 sqlx-sqlite 不用 spawn_blocking

《Tokio》第 16.4 节有反面教材——所有 SQLite 调用用 spawn_blocking 会把 Tokio 的 blocking pool 打爆——因为 SQLite 调用在 100+ QPS 下数量庞大、每次 spawn 都花几 us 进入 blocking pool、还和其他 tokio::fs 等阻塞任务抢 worker 线程。

专用 worker——一条 SQLite 连接 = 一个专属 OS 线程 + mpsc::channel<Command>——每个 SQLite 调用只是channel.send() + channel.recv()——比 spawn_blocking 快 5-10 倍——且不占用 Tokio blocking pool。

读双书的收益——《Tokio》第 16 章让你分辨两种方案的 trade-off;本章给你 sqlx-sqlite 的具体落地——一个阻塞 C 库接入 Tokio 的教科书案例——模式可复制到libpqlibrdkafkalibrocksdb 等任何同步 C 库。

基于 VitePress 构建