Turso + Drizzle + Next.js 15 边缘数据库实战:从零搭建全球低延迟书签应用
它解决什么问题 / 适用场景
Turso 是一个基于 LibSQL(SQLite 的增强分支)的边缘数据库,核心卖点是“零配置”的全球复制。结合 Drizzle ORM 和 Next.js 15,它最适合以下场景:
- 读多写少的全球分发应用:如 CMS、博客、用户仪表盘,读取延迟可降至 <10ms
- 边缘优先的无服务器架构:与 Vercel Edge Functions、Cloudflare Workers 完美配合
- 实时协作工具:共享书签、笔记等,利用 Turso 复制实现快速同步
- IoT 数据聚合点:从边缘节点快速查询设备状态
不适合的场景:需要复杂事务、强一致性写入或大量 JOIN 操作的应用(如金融系统、ERP),建议使用传统 PostgreSQL 或 MySQL。
安装与快速上手
1. 创建 Next.js 项目
BASHnpx create-next-app@latest turso-bookmarks --typescript --tailwind --eslint --app --src-dir cd turso-bookmarks
2. 安装依赖
BASHnpm install drizzle-orm @libsql/client npm install -D drizzle-kit tsx
3. 配置 Turso 数据库
首先安装 Turso CLI(如果尚未安装):
BASHcurl -sSfL https://get.turso.tech/install.sh | sh
登录并创建数据库:
BASHturso auth login turso db create turso-bookmarks --group default
获取连接凭证:
BASHturso db show turso-bookmarks --url turso db tokens create turso-bookmarks
4. 配置环境变量
创建 .env.local 文件:
ENVTURSO_DATABASE_URL=libsql://your-database-name-org.turso.io TURSO_AUTH_TOKEN=your-auth-token-here
安全警告:永远不要在客户端代码中暴露 TURSO_AUTH_TOKEN,仅通过环境变量在服务端使用。
5. 定义数据库 Schema
创建 src/db/schema.ts:
TYPESCRIPTimport { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; export const bookmarks = sqliteTable('bookmarks', { id: integer('id').primaryKey({ autoIncrement: true }), title: text('title').notNull(), url: text('url').notNull(), description: text('description'), createdAt: text('created_at').default('CURRENT_TIMESTAMP'), }); export const tags = sqliteTable('tags', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull().unique(), }); export const bookmarkTags = sqliteTable('bookmark_tags', { bookmarkId: integer('bookmark_id').references(() => bookmarks.id), tagId: integer('tag_id').references(() => tags.id), });
6. 初始化数据库连接
创建 src/db/index.ts:
TYPESCRIPTimport { createClient } from '@libsql/client'; import { drizzle } from 'drizzle-orm/libsql'; const client = createClient({ url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN!, }); export const db = drizzle(client);
7. 运行迁移
创建 drizzle.config.ts:
TYPESCRIPTimport type { Config } from 'drizzle-kit'; export default { schema: './src/db/schema.ts', out: './drizzle', dialect: 'sqlite', dbCredentials: { url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN!, }, } satisfies Config;
生成并推送迁移:
BASHnpx drizzle-kit generate npx drizzle-kit push
核心配置 / 参数说明
环境变量
| 参数名 | 必填 | 说明 |
|---|---|---|
TURSO_DATABASE_URL | 是 | Turso 数据库连接 URL,格式如 libsql://your-database-name-org.turso.io |
TURSO_AUTH_TOKEN | 是 | Turso 认证令牌,通过 turso db tokens create 获取 |
API 路由参数
| 参数 | 位置 | 必填 | 说明 |
|---|---|---|---|
search | GET /api/bookmarks 查询参数 | 否 | 根据标题或描述过滤书签(不区分大小写) |
tag | GET /api/bookmarks 查询参数 | 否 | 根据关联的标签名称过滤书签 |
title | POST /api/bookmarks 请求体 | 是 | 新书签的标题 |
url | POST /api/bookmarks 请求体 | 是 | 新书签的 URL |
description | POST /api/bookmarks 请求体 | 否 | 新书签的描述 |
tagIds | POST /api/bookmarks 请求体 | 否 | 与新书签关联的标签 ID 数组 |
Turso CLI 参数
| 参数 | 说明 |
|---|---|
--group | 用于 turso db create 命令,指定数据库的组/区域进行复制,默认使用最近的区域 |
与同类方案对比
| 对比维度 | Turso (LibSQL) | pg-mcp (PostgreSQL) |
|---|---|---|
| 数据库类型 | 分布式 SQLite | 传统 PostgreSQL |
| 部署模型 | 边缘优先、全球复制 | 通常是单区域部署 |
| 写入性能 | 受限于主区域,延迟较高 | 写入性能更稳定 |
| 读取延迟 | 边缘节点可 <10ms | 取决于服务器位置 |
| 数据一致性 | 最终一致性(复制延迟几秒到几十秒) | 强一致性 |
| 适用场景 | 读多写少、全球分布的应用 | 需要复杂查询和事务的应用 |
Turso 的杀手锏:无需手动管理分片或副本,自动全球复制,对开发者极其友好。
生产环境实践与注意事项
写入瓶颈与解决方案
所有写入操作必须经过主区域,如果主区域距离用户较远,写入延迟会显著增加。解决方案:
- 使用 Drizzle ORM 的事务序列化写入:
TYPESCRIPTimport { db } from '@/db'; import { bookmarks, tags, bookmarkTags } from '@/db/schema'; export async function createBookmark(data: { title: string; url: string; tagIds?: number[] }) { return db.transaction(async (tx) => { const [bookmark] = await tx.insert(bookmarks).values({ title: data.title, url: data.url, }).returning(); if (data.tagIds?.length) { await tx.insert(bookmarkTags).values( data.tagIds.map(tagId => ({ bookmarkId: bookmark.id, tagId })) ); } return bookmark; }); }
- 实现重试逻辑(使用
p-retry库):
TYPESCRIPTimport pRetry from 'p-retry'; const result = await pRetry(() => createBookmark(data), { retries: 3, onFailedAttempt: error => { console.log(`Attempt ${error.attemptNumber} failed.`); }, });
连接超时配置
TYPESCRIPTconst client = createClient({ url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN!, timeout: 10000, // 10秒超时 });
缓存策略
对于频繁读取的数据,使用 Vercel Edge Config 或 KV 存储缓存:
TYPESCRIPTimport { kv } from '@vercel/kv'; export async function getBookmarksCached() { const cacheKey = 'bookmarks:all'; const cached = await kv.get(cacheKey); if (cached) return cached; const bookmarks = await db.select().from(bookmarks).all(); await kv.set(cacheKey, bookmarks, { ex: 60 }); // 缓存60秒 return bookmarks; }
常见报错与排查
Error: LibSQL database file is locked (SQLITE_BUSY)
原因:多个写入操作同时尝试修改数据库。
解决方案:
- 使用 Drizzle ORM 的事务序列化写入操作
- 在写入密集型操作中实现重试逻辑
- 考虑将写入操作路由到主区域,避免从边缘节点直接写入
- 对于高并发场景,使用 Turso 的“嵌入式副本”功能
Error: Connection timeout when connecting to Turso
原因:网络问题或 Turso 服务暂时不可用。
解决方案:
- 检查网络连接,确保可以访问 turso.io
- 验证
TURSO_DATABASE_URL和TURSO_AUTH_TOKEN是否正确 - 增加连接超时时间(如上文配置)
- 实现指数退避重试策略
- 检查 Turso 服务状态页面:https://status.turso.io
Error: Drizzle Kit migration fails with 'relation already exists'
原因:迁移文件与数据库当前状态不一致。
解决方案:
- 检查
drizzle文件夹中的迁移文件,确保没有重复 - 使用
npx drizzle-kit drop删除冲突的表(谨慎操作,会丢失数据) - 更安全的方法:在本地开发数据库上运行
npx drizzle-kit push同步状态,然后重新生成迁移文件 - 对于生产环境,永远不要手动修改数据库 schema,始终通过迁移文件管理
Error: Edge Function execution timeout (Vercel)
原因:Turso 查询在边缘函数中执行时间超过 Vercel 的限制(免费版 10秒,Pro 版 30秒)。
解决方案:
- 优化查询,使用索引和限制返回行数
- 将复杂查询(如全文搜索)移到服务器端 API 路由,而不是边缘函数
- 使用 Turso 的“嵌入式副本”功能,在本地缓存数据以减少网络延迟
- 考虑使用 Vercel 的“Serverless Functions”而不是“Edge Functions”来处理耗时操作
常见问题 FAQ
Q: Turso 的免费版有哪些限制?适合生产环境吗?
A: Turso 免费版包括:1 个数据库、500MB 存储空间、10 个副本、每月 100 万次读取和 10 万次写入。对于小型项目、原型或个人使用来说足够。但对于生产环境,建议升级到付费计划以获得更高的限制、SLA 支持和更低的延迟。免费版的主要限制是写入次数和存储空间,如果应用有大量写入或需要存储大量数据,付费版是必要的。
Q: 如何确保 Turso 数据库的备份和灾难恢复?
A: Turso 提供内置的复制和快照功能:
- 使用
turso db snapshot create <database-name>创建手动快照 - 配置自动快照(在 Turso 控制台设置)
- 使用
turso db shell <database-name> .dump导出 SQL 转储作为额外备份 - 考虑使用 Drizzle Kit 的
drizzle-kit push在另一个 Turso 数据库上重建 schema,然后导入数据 - 对于关键数据,建议实施定期备份策略,并将备份存储在外部存储(如 S3)中
Q: 在 Next.js 中,我应该何时使用 Edge Functions 而不是 Serverless Functions 来查询 Turso?
A: 选择 Edge Functions 当:
- 查询是只读的,且对延迟极度敏感(例如用户仪表盘、首页内容)
- 查询结果可以容忍最终一致性(几秒的延迟)
- 查询逻辑简单,不需要大量计算
选择 Serverless Functions 当:
- 查询涉及写入操作,需要强一致性
- 查询逻辑复杂,需要长时间执行(超过 Edge Functions 的 30 秒限制)
- 需要访问 Node.js 原生模块或文件系统
- 需要处理文件上传或流式响应
一个常见的模式是:使用 Edge Functions 进行读取操作,使用 Serverless Functions 进行写入操作。