秒杀传统数据库! Cloudflare D1 + Drizzle 组合拳,高并发高可用,让我们的成本爆降 10 倍 - D1 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
leia
V2EX    分享发现

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

  •  
  •   leia 2025 年 7 月 10 日 3758 次点击
    是一个创建于 276 天前的主题,其中的信息可能已经有所发展或是发生改变。

    秒杀传统数据库! 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-月

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

    • 读取成本:每天 500 万行的读取量,一个月约 1.5 亿行,完全在免费额度内。即使你的应用流量翻倍,达到每天 1000 万行读取,每月 3 亿行,超出免费额度的 5000 万行只需要额外支付 $0.05/月。
    • 写入成本:每天 10 万行的写入,一个月约 300 万行,远低于免费额度的 5000 万行。即使写入量增长 10 倍,仍然在免费额度内。
    • 存储成本:5GB 的存储空间完全免费。对于大多数中小型应用来说,这已经足够存储数百万条记录

    付费版的价格是 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: { 'ContentType': 'application/json' } }) } return new Response('Not Found', { status: 404 }) } } 

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

    本地开发与调试

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

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

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

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

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

    结束

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

    27 条回复    2025-07-13 20:09:03 +08:00
    summerwar
        1
    summerwar  
       2025 年 7 月 10 日
    cloudflare d1 这种数据库的一个坑:一个 SELECT ... LIMIT 10 OFFSET 90000 的查询,可能会导致 D1 计算接近 90,010 次“行读取”。

    这个时候尽量用游标分页,就可以避免。
    julyclyde
        2
    julyclyde  
       2025 年 7 月 10 日
    @summerwar
    是按行读取收费吗?还是行读取多了会严重性能下降呢?
    summerwar
        3
    summerwar  
       2025 年 7 月 10 日   1
    @julyclyde 是按行读取收费

    其实这个不是 d1 的坑,offset 在 mysql 、sqlite 中也都是读取这么多行,只不过因为 d1 是按行读取计费,所以显得这条命令变成了坑。
    chihiro2014
        4
    chihiro2014  
       2025 年 7 月 10 日
    有了 cf ,基本连服务器钱都不需要了
    FlashEcho
        5
    FlashEcho  
       2025 年 7 月 10 日
    Craveu 不是 cozyai 的吗?为什么这是你的站点?
    june4
        6
    june4  
       2025 年 7 月 10 日
    @summerwar 根本就不应该允许用户翻这么多页。真有这类需求也有高效的分页方案,比如按一个字段排序分页。
    streamrx
        7
    streamrx  
       2025 年 7 月 10 日 via iPhone
    D1 延迟挺高的。 用 druable objects + sqlite 比 d1 好用
    cookii
        8
    cookii  
       2025 年 7 月 10 日 via Android
    我看到有个老哥吐槽,count 一下就是几美刀
    qingmeng
        9
    qingmeng  
       2025 年 7 月 10 日
    免费版 5GB 是十个库的额度,单库只有 500MB ,完全不够用
    lloovve
        10
    lloovve  
       2025 年 7 月 10 日 via iPhone
    全球分布式数据库?一个节点数据能实时同步到其他节点吗?其他节点能实时同步过来?
    iX8NEGGn
        11
    iX8NEGGn  
       2025 年 7 月 10 日
    D1 延迟挺高,不能高并发吧,而且不支持交互式事务,这才是致命缺陷,不过个人项目玩玩倒无所谓。
    RedBeanIce
        12
    RedBeanIce  
       2025 年 7 月 11 日
    @chihiro2014worker 好像只能 nodejs?
    pottwr
        13
    pottwr  
       2025 年 7 月 11 日
    适合 MVP 阶段,确定要真正投入时间做产品的话还是谨慎使用
    hanguofu
        14
    hanguofu  
       2025 年 7 月 11 日 via Android
    谢谢分享宝贵经验
    ttthys
        15
    ttthys  
       2025 年 7 月 11 日
    按行收费的话那不是 count 一下就过免费额度了,还以为是按返回的行收费
    ersic
        16
    ersic  
       2025 年 7 月 11 日
    数据多了别 count ,算全表总行数的读
    bigtear
        17
    bigtear  
       2025 年 7 月 11 日 via Android
    无服务器方案到最后都是后悔
    user1284
        18
    user1284  
       2025 年 7 月 11 日
    又秒杀上了
    wenjie83
        19
    wenjie83  
       2025 年 7 月 11 日
    这种付费方式,不仅要单独根据付费特性去优化语句,而且如果以后付费规则发生变动,是迁移还是不迁移?
    感觉还是买断制放心
    elevioux
        20
    elevioux  
       2025 年 7 月 11 日   1
    这个账号专门给自己博客引流的吗?
    chihiro2014
        21
    chihiro2014  
       2025 年 7 月 11 日
    @RedBeanIce worker 支持的语言都可以操作 D1 吧,nodejs 只是 worker 用的频繁罢了。rust 啥的也都可以
    leia
        22
    leia  
    OP
       2025 年 7 月 11 日
    @elevioux 没有了 哈哈哈 老,
    leia
        23
    leia  
    OP
       2025 年 7 月 11 日
    @yb2313 包秒的
    molvqingtai
        24
    molvqingtai  
       2025 年 7 月 11 日
    默认不是分布式,延迟太高了,国内一个请求 500+ms
    Foxkeh
        25
    Foxkeh  
       2025 年 7 月 11 日
    "成本暴降 10 倍"?
    Selenium39
        26
    Selenium39  
       2025 年 7 月 11 日
    一股 AI 味
    leia
        27
    leia  
    OP
       2025 年 7 月 13 日
    @Selenium39 原文写完让他润色改格式是这样的
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     966 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 22:35 PVG 06:35 LAX 15:35 JFK 18:35
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86