4.1 为什么需要嵌入式副本
普通客户端模式下,每次查询都要打一次网络——即便是 Edge 副本也要 20-50ms。对下列场景这还不够快:
- 移动 App:离线可用、列表要无感快
- Electron/Desktop 工具:本地瞬时响应
- Next.js SSR:每个请求打 10+ 次数据库,串起来就是 500ms
- AI 助手/Agent:需要本地快速检索大量上下文
嵌入式副本的思路:在你的应用进程内保留一份完整的 SQLite 文件,通过 WAL 协议从远端异步同步。读走本地(几微秒),写仍路由到远端 primary。
4.2 架构示意
┌───────────────────── Your App Process ─────────────────┐
│ │
│ libSQL Client │
│ ┌─────────────────────────┐ │
read──▶ │ │ Local SQLite file │ │
(μs) │ │ (./local.db) │ │
│ └──────────┬──────────────┘ │
write──▶│ │ background sync (HTTP) │
│ ▼ │
└──────────────┼───────────────────────────────────────────┘
│
▼
┌─────────────┐
│ Turso cloud │
│ primary │
└─────────────┘
4.3 最小示例
import { createClient } from '@libsql/client';
const db = createClient({
url: 'file:./local.db', // 本地 SQLite 文件
syncUrl: process.env.TURSO_DATABASE_URL!, // 远端 Turso
authToken: process.env.TURSO_AUTH_TOKEN!,
syncInterval: 60, // 每 60 秒自动同步一次
});
// 第一次运行要手动拉满数据
await db.sync();
// 读:走本地,几微秒
const { rows } = await db.execute('SELECT * FROM users');
// 写:自动转发到远端 primary,写成功后本地也更新
await db.execute('INSERT INTO users(name) VALUES(?)', ['Alice']);
关键三个字段:
- url: 'file:./local.db'
- 本地 SQLite 文件路径。目录不存在会创建。
- syncUrl
- 远端 Turso URL。有了它就从"纯本地 SQLite"变成"嵌入式副本"。
- syncInterval(秒)
- 后台自动 sync 的间隔。不设 = 只在
db.sync()手动触发时才同步。
4.4 sync() 行为解析
const result = await db.sync();
// { frames_synced: 42, frame_no: 1234 }
- 第一次 sync 会全量拉取——本地文件从零开始构建
- 后续 sync 只拉 WAL frame 增量
- sync 期间本地查询仍然能跑(读旧 snapshot)
- sync 失败(网络掉线)应用仍正常工作,读本地
4.5 写路径深入
嵌入式副本不是"本地写,再推上去"——那样会产生冲突。实际路径:
- 客户端发
INSERT到远端 primary(HTTP) - primary 返回成功 + 新的 frame 编号
- 客户端拉取这个 frame 并 apply 到本地
- 本地 SQLite 现在包含这条新记录
写的往返延迟仍是网络级别(100ms-200ms);但写之后本地立刻能读到——因为客户端主动拉了新 frame。
4.6 read-your-writes 保证
与普通副本模式不同,嵌入式副本天生提供"读自己写"保证:写完立即读,肯定看到自己刚插入的数据。(原理见上一节)。
4.7 离线与重连
try {
await db.sync();
} catch (e) {
console.warn('offline, using local snapshot');
}
// 离线期间可以读本地
await db.execute('SELECT * FROM cache');
// 离线期间若写会抛 LibsqlError(primary 不可达)
如果离线期间必须写,思路是:先写到另一张 本地 outbox 表;联网后应用代码把 outbox 刷到 primary。libSQL 目前不内置这种 sync,需要自己管。
4.8 多进程访问同一文件
嵌入式副本的本地文件 = 普通 SQLite 文件——同一进程可以多 client 并发使用,但不同进程之间要小心:
- 只读多进程:OK(WAL 模式)
- 多进程都做 sync:可能互相覆盖 frame 状态。推荐一个"sync owner"进程,其他进程纯读
4.9 Mobile:iOS / Android
libSQL 提供 Swift 和 Kotlin SDK,嵌入式副本接口一致:
// Swift
let db = try Database.embedded(
path: "local.db",
url: TURSO_URL,
authToken: TURSO_TOKEN
)
try await db.sync()
let users = try await db.query("SELECT * FROM users")
典型用法:App 启动时 sync() 一次、切到前台再 sync()、涉及 Cloud 变更的动作完成后 sync()。
4.10 大小与存储
本地文件 = 全量数据——如果服务端有 10GB,本地也会有 10GB。对 Mobile 来说这太大了。实务做法:
- db-per-tenant:每个用户一个库,只 sync 自己那个(Turso 允许单账号数百个库)
- view/shard:按地区/时间分表;App 只 sync 当前用户相关的表(Turso 暂不支持 partial sync,需要应用层分库实现)
4.11 Next.js / SSR 场景
Node 版 Next.js 服务器可以用嵌入式副本——启动时 sync 一次,SSR 读直接命中本地文件:
// app/db.ts
import { createClient } from '@libsql/client';
export const db = createClient({
url: 'file:./data/replica.db',
syncUrl: process.env.TURSO_URL!,
authToken: process.env.TURSO_TOKEN!,
syncInterval: 10,
});
// 启动时全量 sync
await db.sync();
效果:首屏 SSR 从 10+ 次网络 DB 调用(~500ms)变成全本地(~5ms)。
Vercel Serverless 函数磁盘每次冷启动清空——嵌入式副本没意义。嵌入式副本适合长驻进程:Fly.io、自己 VPS、Node server、Desktop、Mobile。Vercel/Netlify 走普通 HTTP 客户端更合适。
4.12 加密:Encryption at Rest
const db = createClient({
url: 'file:./local.db',
syncUrl,
authToken,
encryptionKey: process.env.ENC_KEY!,
});
本地文件用 SEE 兼容 AES 加密。key 安全由你负责——建议走平台 keychain(iOS Keychain / Android Keystore)。
小结
嵌入式副本是 Turso 的灵魂功能——把"SQL 数据库"缩到应用进程内,读取接近零延迟,离线可用。关键 API:url: 'file:...' + syncUrl + db.sync()。最合适的场景是长驻进程(Server/Mobile/Desktop),不适合 Serverless。下一章接 ORM——用 Drizzle 把它整个 TypeScript 化。