Appearance
第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 sqlite3handle、主线程通过 flume channel 发Command给 worker、worker 回Result给主线程。 ConnectionWorker(sqlx-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>withfair = 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 的两条通信通道:
command_tx: flume::Sender—— 主线程→worker,发命令。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(¶ms) {
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?
})
}四步:
- 创建 flume channel(主线程到 worker)+ oneshot(worker 到主线程的初始化结果)。
- Spawn OS 线程——在里面打开 sqlite3(C API 调用)。
- 通过 oneshot 把 ConnectionWorker handle 送回主线程。
- 进入消息泵循环——直到 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 内部:
- 主线程调
shared.conn.lock().await—— 获取 worker 的 conn lock。 - Worker 线程此时通过某种机制等 lock 释放——sqlx 用
Command::UnlockDb命令让 worker 明确释放 lock、等主线程操作完再 re-lock。 - 主线程拿到 MutexGuard、可以访问 conn。
Mutex::new(conn, true) 的 true 是 fair 参数——必须 fair,因为 Command::UnlockDb 让 worker 释放后立即尝试 re-lock——非 fair 锁会让 worker 立刻抢回、用户的 lock_handle 永远拿不到。
这条细节是 sqlx-sqlite 作者踩坑之后的修复——注释原文(worker.rs:123-126):
note: must be fair because in
Command::UnlockDbwe 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 journal | WAL |
|---|---|---|
| 读写并发 | 互斥 | 读写分离(可并发) |
| 吞吐 | 低 | 高(10-100× 写吞吐) |
| 读时延 | 写时被阻塞 | 近零阻塞 |
| fsync | 每事务 | 也需要(可调 synchronous) |
| 崩溃恢复 | rollback journal | checkpoint 机制 |
| 跨 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_LOCK → SHARED → RESERVED → PENDING → EXCLUSIVE —— 按严格顺序升级。
- 多读可以并发(SHARED)。
- 写必须 EXCLUSIVE——和所有读互斥。
当一个进程 / 连接持有 EXCLUSIVE、另一个连接尝试写——后者返回 SQLITE_BUSY。
busy_timeout 让 SQLITE_BUSY 返回前 SQLite 先等一段时间:
rust
SqliteConnectOptions::new()
.busy_timeout(Duration::from_secs(5))
// 等锁 5 秒、超时 SQLITE_BUSYsqlx 的 SqliteConnectOptions 默认 busy_timeout = 5s(sqlx-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:
| Class | Rust 对应 |
|---|---|
| NULL | Option::None |
| INTEGER | i64(最大 8 字节) |
| REAL | f64 |
| TEXT | String(UTF-8) |
| BLOB | Vec<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 的独特架构:
- SQLite 的本质(§18.1)—— 无网络、C API 同步、单文件、内嵌。和 Postgres/MySQL 完全不同。
- ConnectionWorker 架构(§18.2)—— 专用 OS 线程 + flume channel,让同步 C API 变 async。
- Command 枚举 + 回复 channel(§18.3)—— 每 variant 带 oneshot 或 flume sender。Execute 用 flume(流式)、其他用 oneshot。
- 消息泵循环(§18.4)—— worker 线程串行处理 command、内部全同步 C API。
- ConnectionHandle + unsafe Send(§18.5)—— safe/unsafe 边界、手动标注 Send(单线程独占保证)。
- Mutex + fair 锁(§18.6)—— LockedSqliteHandle 让高级用户借出 conn;fair 语义防止 worker 抢回。
- WAL 模式(§18.7)—— 读写并发、生产必开。
- busy_timeout(§18.8)—— 多进程共享时的等锁时限、sqlx 默认 5s 已合理(覆盖 C API 原生 0 的默认)。
- SQLite 类型系统(§18.9)—— 5 种 storage class、极简但 nullability 弱。
- SQLite 独有特性(§18.10)—— in-memory、ATTACH、Serialize、collation、update hook。
- 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?;八条配置的意义:
create_if_missing(true)—— 文件不存在自动建。journal_mode(Wal)—— WAL 模式、并发提升数倍。synchronous(Normal)—— 每事务 fsync WAL,不 fsync DB 本身——大幅加速且不丢已 commit 数据。busy_timeout(5s)—— 多进程 / 多连接时等锁。foreign_keys(true)—— SQLite 默认关外键检查!必须显式打开。cache_size -20000—— 负数表示 KB、正数表示页数。-20000= 20MB。默认 2MB、生产应该调大。temp_store memory—— 临时表用内存而不是磁盘——SORT / DISTINCT 等操作加速。- 日志配置——和 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 生态集成。
对比:
| 维度 | rusqlite | sqlx-sqlite |
|---|---|---|
| 同步/异步 | 同步(用户自己 spawn) | 异步(sqlx 封装 worker) |
| API 风格 | 直接 SQLite API | sqlx Executor 统一 |
| 跨 DB 代码 | 不支持 | 支持(sqlx-core) |
| query! 宏 | 无 | 有 |
| 类型系统 | 自己的 trait | sqlx 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
读完本章你应该能:
- 解释为什么 SQLite 不能像 Postgres 那样走 async 协议。
- 描述 ConnectionWorker 架构——worker 线程 + channel 通信。
- 说出 Command enum 每种 variant 的大致作用。
- 理解 Mutex fair 锁的必要性(UnlockDb 场景)。
- 配置生产级 SQLite(WAL + busy_timeout + cache_size)。
- 选择何时用 SQLite 何时用其他 DB。
- 对比 sqlx-sqlite 和 rusqlite 的适用场景。
- 用
:memory:写 sqlx 测试。 - 理解"包装同步库成 async"的通用模式、迁移到自己项目。
这 9 条能力让你成为 Rust 生态里会用也懂 SQLite 的工程师——绝大多数 Rust 开发者只会用、不懂底层——你多了这一层理解。
下一章 Any 驱动——讨论 sqlx 的"运行时多态"——让一份代码同时跑三家 DB。这是整个第五部分的收官——把抽象和具体两端在 Any 里再次串起来。
18.19 SQLite 的 WAL 细节
WAL 模式是 SQLite 生产配置的核心、值得展开讲:
WAL 的工作机制:
- 写事务 append 到
*-wal文件(DB 文件不变)。 - 读事务直接读 DB 文件 + 读 WAL(没被 checkpoint 的部分)。
- 定期 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-sys的bundledfeature)。编译时自动编 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.4 | Worker 同步执行 C API、串行处理 commands |
| ConnectionHandle FFI | §18.5 | unsafe Send 标注 + Drop 里 close_v2 |
| Mutex fair 锁 | §18.6 | UnlockDb 场景、必须 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.10 | SQLite 独有特性 |
| run 路径 | §18.11 | async 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 的核心数据结构 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>,
}两条通道:
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::step(sqlx-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_SHAREDCACHE→ shared cache 被另一连接锁住—— 调unlock_notify::wait等通知、sqlite3_reset重置语句、循环重试。- 其他—— 转
SqliteError返回。
unlock_notify::wait 是 sqlite 特有机制—— 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 行代码里能学到:
unsafe块尽量小—— 只包 FFI 调用、其他逻辑在 safe Rust 写。- SAFETY 注释是契约—— 作者对 reviewer 的承诺——不写就是耍流氓。
- 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 线程 + channel | sqlx-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 的教科书案例——模式可复制到libpq、librdkafka、librocksdb 等任何同步 C 库。