秒杀传统数据库! Cloudflare D1 + Drizzle 组合拳,高并发高可用,让我们的成本爆降 10 倍 - D1

61 天前
 leia

秒杀传统数据库! Cloudflare D1 + Drizzle 组合拳,高并发高可用,让我们的成本爆降 10 倍 - D1

想象一下:我们的应用用户量稳步增长,​传统数据库的成本和维护压力也随之上升​。而在这个时代,有没有更高效、更经济的数据库解决方案?​Cloudflare D1 结合 Drizzle ORM 的组合​,正在为众多​出海应用开发提供一条全新的技术路径​。

传统数据库方案在高并发场景下往往需要复杂的扩容、分片和负载均衡,成本随着流量呈指数级增长。而当我们了解了 Cloudflare D1 这款​基于 SQLite 构建的边缘数据库​,再配合 Drizzle 这个轻量级 ORM 的强大能力,我们会惊讶于这个组合如何能在保持高性能的同时,​将我们的基础设施成本直接腰斩​!

无需复杂的数据库集群,无需昂贵的专用服务器,无需担心地理位置带来的延迟问题 — 这个方案将​彻底改变我们对数据库架构的认知​。

Cloudflare D1 实战:从零开始搭建高性能数据库

Cloudflare D1 是 Cloudflare 推出的一款分布式 SQL 数据库,它基于 SQLite 构建,完全集成在 Cloudflare Workers 生态系统中。D1 将 SQLite 数据库部署到 Cloudflare 的全球边缘网络,让我们的数据库与应用代码一样,运行在离用户最近的位置,大幅降低延迟。

D1 成本计算与对比

在深入技术细节前,让我们先来看看 D1 在成本方面的巨大优势。Cloudflare D1 采用了极具竞争力的定价模型:

资源类型 Workers Free (免费版) Workers Paid (付费版)
读取行数 每天 500 万行限制 每月前 250 亿行免费,超出部分 $0.001/百万行
写入行数 每天 10 万行限制 每月前 5000 万行免费,超出部分 $1.00/百万行
存储空间 总计 5GB 限制 前 5GB 免费,超出部分 $0.75/GB-月

让我们来分析一下免费版的套餐:

付费版的价格是 5$,免费版的规模足够处理 5000-20000 日活的应用,付费 20000-100w 日活。

快速上手 D1

1. 安装 Wrangler CLI

首先,我们需要安装 Cloudflare 的 Wrangler CLI 工具:

npm install -g wrangler

2. 创建 D1 数据库

登录我们的 Cloudflare 账户后,创建一个新的 D1 数据库:

wrangler login
wrangler d1 create my-database

执行后,我们会看到类似这样的输出:

✅ Successfully created DB 'my-database' in region APAC
Created D1 database 'my-database' with id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

请记下这个数据库 ID ,我们后续会用到。

3. 配置 wrangler.toml

在我们的项目根目录创建或编辑 wrangler.toml 文件,添加 D1 数据库配置:

[[d1_databases]]
binding = "DB" # 在 Workers 中使用的变量名
database_name = "my-database"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # 替换为我们的数据库 ID

4. 创建数据表

创建一个 SQL 文件,例如 schema.sql

CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

然后执行:

wrangler d1 execute my-database --file=./schema.sql

5. 在 Workers 中使用 D1

现在,我们可以在 Cloudflare Workers 中使用 D1 数据库了:

export interface Env {
  DB: D1Database
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // 查询用户列表
    const { results } = await env.DB.prepare('SELECT * FROM users ORDER BY created_at DESC LIMIT 10').all()

    return new Response(JSON.stringify(results), {
      headers: { 'Content-Type': 'application/json' }
    })
  }
}

D1 的实用命令与简单实践

在实际开发中,我们需要更多的工具来管理数据库。D1 提供了一系列强大的命令行工具,让数据库管理变得轻松高效。 数据库迁移:管理我们的架构变更 数据库结构会随着需求不断变化。D1 提供了完善的迁移系统,让我们可以版本化管理数据库结构:

创建一个新的迁移文件
wrangler d1 migrations create my-database add_user_role

这会在项目中创建一个类似 migrations/0001_add_user_role.sql` 的文件。编辑这个文件,添加我们的 SQL 变更:

-- Migration: add_user_role
-- Created at: 2023-10-15 14:30:00

-- 向用户表添加角色字段
ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user' NOT NULL;

-- 创建一个新的角色权限表
CREATE TABLE role_permissions (
  role TEXT NOT NULL,
  permission TEXT NOT NULL,
  PRIMARY KEY (role, permission)
);

然后应用这些迁移:

应用到本地开发环境
wrangler d1 migrations apply my-database --local

应用到生产环境
wrangler d1 migrations apply my-database --remote

这种方式让我们可以: - 追踪数据库的所有变更历史 - 在团队中同步数据库结构 - 在不同环境(开发、测试、生产)之间保持一致性

数据导入导出:备份与恢复

需要备份数据或将数据迁移到其他环境? D1 提供了简单的导出导入功能

导出整个数据库(结构+数据)
wrangler d1 export my-database --output=backup.sql

只导出特定表
wrangler d1 export my-database --table=users --output=users_backup.sql

只导出结构,不导出数据
wrangler d1 export my-database --output=schema.sql --no-data

导入数据同样简单:

wrangler d1 execute my-database --file=backup.sql

实战案例:构建一个博客系统

让我们通过一个实际案例来展示 D1 的强大功能。假设我们要构建一个简单的博客系统,需要存储文章和评论。

首先,创建数据库结构:

-- migrations/0001_create_blog_tables.sql
CREATE TABLE posts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  author_id INTEGER NOT NULL,
  published_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  status TEXT DEFAULT 'draft' NOT NULL
);

CREATE TABLE comments (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  post_id INTEGER NOT NULL,
  author_name TEXT NOT NULL,
  content TEXT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
);

CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_comments_post_id ON comments(post_id);

npx wrangler d1 execute prod-d1-tutorial --local --file=./migrations/0001_create_blog_tables.sql

然后,在 Workers 中实现 API 接口:

export interface Env {
  DB: D1Database
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url)
    const path = url.pathname

    // 获取博客文章列表
    if (path === '/api/posts' && request.method === 'GET') {
      const { results } = await env.DB.prepare(
        "SELECT id, title, published_at FROM posts WHERE status = 'published' ORDER BY published_at DESC LIMIT 10"
      ).all()

      return new Response(JSON.stringify(results), {
        headers: { 'Content-Type': 'application/json' }
      })
    }

    // 获取单篇文章及其评论
    if (path.match(/^\/api\/posts\/\d+$/) && request.method === 'GET') {
      const postId = path.split('/').pop()

      // 获取文章详情
      const post = await env.DB.prepare('SELECT * FROM posts WHERE id = ?').bind(postId).first()

      if (!post) {
        return new Response(JSON.stringify({ error: 'Post not found' }), {
          status: 404,
          headers: { 'Content-Type': 'application/json' }
        })
      }

      // 获取文章评论
      const { results: comments } = await env.DB.prepare(
        'SELECT * FROM comments WHERE post_id = ? ORDER BY created_at DESC'
      )
        .bind(postId)
        .all()

      return new Response(JSON.stringify({ post, comments }), {
        headers: { 'Content-Type': 'application/json' }
      })
    }

    // 添加评论
    if (path.match(/^\/api\/posts\/\d+\/comments$/) && request.method === 'POST') {
      const postId = path.split('/')[3]
      const { author_name, content } = await request.json()

      // 插入评论
      const result = await env.DB.prepare(
        'INSERT INTO comments (post_id, author_name, content) VALUES (?, ?, ?) RETURNING id'
      )
        .bind(postId, author_name, content)
        .run()

      return new Response(JSON.stringify({ id: result.results[0].id }), {
        status: 201,
        headers: { 'Content-Type': 'application/json' }
      })
    }

    return new Response('Not Found', { status: 404 })
  }
}

这个简单的博客 API 已经能够: - 获取已发布的文章列表 - 获取单篇文章及其评论 - 为文章添加新评论

本地开发与调试

在开发过程中,我们可以使用本地数据库进行测试:

启动本地开发服务器,使用本地 D1 数据库
wrangler dev --local

这会在本地创建一个 SQLite 数据库文件,我们可以在开发过程中使用它,而不需要每次都操作远程数据库。当我们的代码准备好后,再将变更应用到远程数据库。

应用迁移到远程数据库
wrangler d1 migrations apply my-database --remote

通过这种方式,我们可以在本地快速迭代开发,同时确保生产环境的数据安全。

结束

而在下一章节中,就讲解``Drizzle\,讲这个的主要目的是为了给大家普及一下`海外批量应用`的基础套件的知识 关于我

2721 次点击
所在节点    分享发现
27 条回复
summerwar
61 天前
cloudflare d1 这种数据库的一个坑:一个 SELECT ... LIMIT 10 OFFSET 90000 的查询,可能会导致 D1 计算接近 90,010 次“行读取”。

这个时候尽量用游标分页,就可以避免。
julyclyde
61 天前
@summerwar
是按行读取收费吗?还是行读取多了会严重性能下降呢?
summerwar
61 天前
@julyclyde 是按行读取收费

其实这个不是 d1 的坑,offset 在 mysql 、sqlite 中也都是读取这么多行,只不过因为 d1 是按行读取计费,所以显得这条命令变成了坑。
chihiro2014
61 天前
有了 cf ,基本连服务器钱都不需要了
chesha1
61 天前
Craveu 不是 cozyai 的吗?为什么这是你的站点?
june4
61 天前
@summerwar 根本就不应该允许用户翻这么多页。真有这类需求也有高效的分页方案,比如按一个字段排序分页。
streamrx
61 天前
D1 延迟挺高的。 用 druable objects + sqlite 比 d1 好用
cookii
61 天前
我看到有个老哥吐槽,count 一下就是几美刀
qingmeng
61 天前
免费版 5GB 是十个库的额度,单库只有 500MB ,完全不够用
lloovve
61 天前
全球分布式数据库?一个节点数据能实时同步到其他节点吗?其他节点能实时同步过来?
iX8NEGGn
61 天前
D1 延迟挺高,不能高并发吧,而且不支持交互式事务,这才是致命缺陷,不过个人项目玩玩倒无所谓。
RedBeanIce
61 天前
@chihiro2014worker 好像只能 nodejs?
pottwr
61 天前
适合 MVP 阶段,确定要真正投入时间做产品的话还是谨慎使用
hanguofu
61 天前
谢谢分享宝贵经验
ttthys
60 天前
按行收费的话那不是 count 一下就过免费额度了,还以为是按返回的行收费😂
ersic
60 天前
数据多了别 count ,算全表总行数的读
bigtear
60 天前
无服务器方案到最后都是后悔
yb2313
60 天前
又秒杀上了
wenjie83
60 天前
这种付费方式,不仅要单独根据付费特性去优化语句,而且如果以后付费规则发生变动,是迁移还是不迁移?
感觉还是买断制放心
elevioux
60 天前
这个账号专门给自己博客引流的吗?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1144347

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX