个人博客、爬虫控制台、小团队内部工具,数据量不大、部署希望「一个二进制 + 一个 db 文件」时,SQLite 往往是性价比最高的选择。本文结合 Rust 生态里的常见实践,讲清楚怎么用得稳。
为什么选 SQLite
- 零运维:不需要单独起 MySQL/Postgres 进程
- 备份简单:复制
.db文件即可 - 性能够用:读多写少、单机 QPS 在几千以内很常见
- 事务可靠:评论审核、文件元数据更新适合走事务
不适合:多机写入、复杂分析查询、TB 级数据。
基础用法:rusqlite
[dependencies]
rusqlite = { version = "0.32", features = ["bundled"] }
bundled 会把 SQLite 编译进项目,部署时不用系统里预装 libsqlite3。
use rusqlite::{Connection, params};
fn init_db(path: &str) -> rusqlite::Result<Connection> {
let conn = Connection::open(path)?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS uploads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
original_name TEXT NOT NULL,
file_path TEXT NOT NULL,
uploaded_at TEXT NOT NULL DEFAULT (datetime('now'))
);",
)?;
Ok(conn)
}
fn insert_upload(conn: &Connection, name: &str, path: &str) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO uploads (original_name, file_path) VALUES (?1, ?2)",
params![name, path],
)?;
Ok(())
}
始终用 ?1、?2 占位符传参,避免拼接 SQL 引发注入。
连接与并发:单连接 vs 连接池
rusqlite::Connection 默认不能跨线程共享。Web 服务里常见两种方案:
方案 A:每次请求 open(小项目可接受)
方案 B:r2d2 连接池(推荐)
r2d2 = "0.8"
r2d2_sqlite = "0.25"
use r2d2_sqlite::SqliteConnectionManager;
let manager = SqliteConnectionManager::file("data/app.db");
let pool = r2d2::Pool::new(manager).unwrap();
// Handler 里
let conn = pool.get().unwrap();
池子大小一般设为 CPU 核数 或略大即可,SQLite 同一时刻仍只有一个写者,但读可以并发。
迁移:用 SQL 文件管理表结构
把 DDL 放在 migrations/001_init.sql,启动时按版本号执行:
fn migrate(conn: &Connection) -> rusqlite::Result<()> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL);
INSERT INTO schema_version (version) SELECT 0
WHERE NOT EXISTS (SELECT 1 FROM schema_version);",
)?;
let version: i32 = conn.query_row(
"SELECT version FROM schema_version LIMIT 1",
[],
|r| r.get(0),
)?;
if version < 1 {
conn.execute_batch(include_str!("../migrations/001_comments.sql"))?;
conn.execute("UPDATE schema_version SET version = 1", [])?;
}
Ok(())
}
表结构变更只增不改、用 ALTER TABLE 谨慎操作;破坏性变更先导出数据再重建。
评论表设计示例
CREATE TABLE IF NOT EXISTS blog_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_key TEXT NOT NULL,
parent_id INTEGER,
author_name TEXT NOT NULL,
content TEXT NOT NULL,
is_approved INTEGER NOT NULL DEFAULT 0,
is_hidden INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (parent_id) REFERENCES blog_comments(id)
);
CREATE INDEX IF NOT EXISTS idx_comments_post ON blog_comments(post_key);
CREATE INDEX IF NOT EXISTS idx_comments_pending ON blog_comments(is_approved, is_hidden);
post_key 用 blog/slug 这种逻辑键,比自增文章 ID 更稳——文章文件删了重建也不撞车。
实用建议
- WAL 模式:
PRAGMA journal_mode=WAL;提升并发读性能 - 忙等重试:写冲突时
busy_timeout设 3–5 秒 - 定期备份:cron 复制 db 文件,或启动时打快照
- 不要把 db 放在静态目录:通过应用层 API 访问,防止被下载
SQLite 不是「玩具数据库」。Discord、浏览器、手机系统里都有它的身影。把边界划清楚,它能撑起相当多的个人项目。
评论