Featured image of post Serverless 架构下的多数据库选型

Serverless 架构下的多数据库选型

为什么需要三种数据库

RuiToolAI 是一个全栈 SaaS 平台,数据需求多种多样:

  • 用户数据、订单、积分流水:需要结构化查询、关联查询
  • Session、API 缓存、页面缓存:需要极快读写、支持过期时间
  • 用户上传的文件、AI 生成的图片:需要存储大文件、支持 CDN 分发

如果用单一数据库,比如全用 D1,反过来 Session 和缓存会拖慢关系型查询;全用 KV,用户订单这种结构化数据没法做关联查询。

所以选择了 D1 + KV + R2 的组合。

D1:关系型数据库

Cloudflare D1 是基于 SQLite 的 Serverless 关系型数据库,兼容 SQLite 语法。

在 RuiToolAI 中的使用

D1 存储所有结构化数据:

1
2
3
4
5
6
7
user                     # 用户表
session                  # Session 表(已迁移到 KV)
credit_transaction       # 积分流水表
generated_image          # 图片生成记录表
cms_entry                # CMS 内容表
cms_navigation_node      # CMS 导航表
cms_media                # CMS 媒体表

集成方式

使用 Drizzle ORM 操作 D1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { drizzle } from "drizzle-orm/d1";
import { getDB } from "@/db";

const db = getDB();

// 查询
const user = await db.query.userTable.findFirst({
  where: eq(userTable.email, email),
});

// 插入
await db.insert(userTable).values({
  id: `usr_${createId()}`,
  email,
  passwordHash,
  role: "user",
});

局限性

  • 不支持事务:D1 不支持多语句事务,处理退款时要补偿式设计
  • 单文件限制:数据库文件最大 2GB(付费版 10GB)
  • 并发限制:写入并发有限,高并发场景需要设计重试

KV:键值存储

Cloudflare KV 是分布式的键值存储,最终一致性模型,全球秒级同步。

在 RuiToolAI 中的使用

KV 主要用在两个场景:

1. Session 存储

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 存储 Session
await env.NEXT_TAG_CACHE_KV.put(
  `session:${sessionId}`,
  JSON.stringify(sessionData),
  { expirationTtl: 7 * 24 * 60 * 60 } // 7 天过期
);

// 读取 Session
const raw = await env.NEXT_TAG_CACHE_KV.get(`session:${sessionId}`);
const session = raw ? JSON.parse(raw) : null;

2. Vinext 页面缓存

1
2
3
4
// worker-entrypoint.ts
new KVCacheHandler(env.NEXT_INC_CACHE_KV, {
  appPrefix: VINEXT_CACHE_PREFIX
})

Vinext 框架自动用 KV 做 ISR 缓存,每个页面请求都会先查 KV 是否有缓存。

特点

  • 极快读取:全球边缘节点,毫秒级
  • 最终一致性:写入后最多 60 秒同步到全球
  • 免费额度:每天 10 万次读取,1GB 存储

R2:对象存储

Cloudflare R2 是 S3 兼容的对象存储,最大特点是不收出口流量费

在 RuiToolAI 中的使用

R2 存储所有用户生成的文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 存储 AI 生成的图片
await env.USER_UPLOADS_BUCKET.put(r2Key, imageBuffer, {
  httpMetadata: {
    contentType: "image/png",
    contentDisposition: `attachment; filename="${filename}"`,
  },
  customMetadata: {
    prompt: prompt.slice(0, 500),
    generatedBy: userId,
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
  },
});

// 读取文件
const object = await env.USER_UPLOADS_BUCKET.get(r2Key);
const buffer = await object.arrayBuffer();

// 删除文件
await env.USER_UPLOADS_BUCKET.delete(r2Key);

生命周期规则

R2 支持自动清理过期文件。在 RuiToolAI 中,设置了 7 天生命周期规则,配合 Cron 定时任务清理 DB 记录:

1
2
R2 规则:uploads/ 前缀,7 天后自动删除
Cron 任务:每天凌晨 3 点,删除 DB  expiresAt < now 的记录

选型对比表

特性D1KVR2
数据类型关系型(SQLite)键值对文件/对象
读取速度快(毫秒级)极快(毫秒级)取决于文件大小
一致性强一致最终一致强一致
查询能力SQL 完整支持仅 key 查询前缀/列表
关联查询支持不支持不支持
最大大小2GB/10GB1GB(免费)无限
出口流量费无(核心优势)
适合场景用户数据、订单缓存、Session文件存储

实际使用场景

场景一:用户注册

1
用户提交 → D1 INSERT user → KV PUT session → 返回

场景二:AI 图片生成

1
用户提交 → D1 UPDATE credits → D1 INSERT task → Model API → R2 PUT image → D1 UPDATE status

场景三:页面加载

1
用户访问 → KV GET 缓存 → 命中返回 / 未命中 → DB 查询 → 渲染

总结

多数据库不是越多越好,关键是根据数据的访问模式选型:

  • 需要关联查询 → D1
  • 需要极快读写 + 过期 → KV
  • 需要存储大文件 + 免流量 → R2

Cloudflare 的这三件套覆盖了 SaaS 产品 90% 的数据需求,而且全部按量付费,非常适合独立开发者。

参考资源

使用 Hugo 构建
主题 StackJimmy 设计