Chapter 04

嵌入式副本 Embedded Replicas

把 SQLite 文件直接同步到你的应用进程——读取 < 1ms、无网络依赖、完全离线可用。这是 Turso 与其他 Edge 数据库最本质的差异。

4.1 为什么需要嵌入式副本

普通客户端模式下,每次查询都要打一次网络——即便是 Edge 副本也要 20-50ms。对下列场景这还不够快:

嵌入式副本的思路:在你的应用进程内保留一份完整的 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 }

4.5 写路径深入

嵌入式副本不是"本地写,再推上去"——那样会产生冲突。实际路径:

  1. 客户端发 INSERT 到远端 primary(HTTP)
  2. primary 返回成功 + 新的 frame 编号
  3. 客户端拉取这个 frame 并 apply 到本地
  4. 本地 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 并发使用,但不同进程之间要小心

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 来说这太大了。实务做法:

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/无状态 PaaS 要小心

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 化。