Cloudflare D1 数据库实战:SQLite 边缘数据库与全球复制
0.01 毫秒。
这是 SQLite 在本地读取一行数据的时间。同样的查询,跑在 Cloudflare D1 上大约 0.5 毫秒,而 PostgreSQL 跨区域访问可能要 1-3 毫秒——看起来差别不大?但如果你的用户在东京,数据库在弗吉尼亚,光是网络往返就得吃掉 100 多毫秒。
去年我在做一个全球部署的项目时,被这个问题卡住了。传统数据库要么忍受高延迟,要么搞复杂的读写分离。直到 Cloudflare 在 2025 Developer Week 发布了 D1 的全球复制功能,事情开始变得有意思了。
这篇文章我会聊聊 D1 到底是怎么把 SQLite 搬到边缘的,它那些听起来很玄乎的概念——Durable Objects、Lamport 时间戳、Sessions API——实际用起来是什么体验,以及什么时候该选它,什么时候该绕道。
一、D1 是什么:SQLite 跑在边缘的数据库
简单说,D1 就是 Cloudflare 把 SQLite 搬到了他们的边缘网络上,让你可以在全球 300 多个城市的节点里读写数据库。
但如果你以为它只是”SQLite + CDN”这么简单,那就低估它的野心了。传统 SQLite 有几个致命问题让它很难用在生产环境:单文件存储没法分布式部署、没有内置的故障恢复、写操作会锁住整个数据库。D1 针对这些做了重新设计。
它和传统 SQLite 有什么不一样
首先是集成方式。D1 直接跑在 Cloudflare Workers 里,你可以像调用普通函数一样操作数据库:
// wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxx-xxxx-xxxx"
// 在 Worker 里查询
export default {
async fetch(request, env) {
const { results } = await env.DB.prepare(
"SELECT * FROM users WHERE id = ?"
).bind(1).all();
return Response.json(results);
}
}
其次是 Time Travel。D1 会自动保存数据库的历史版本,你可以回滚到任意时间点——这个功能对于 SQLite 来说简直是奢侈。免费账户保留 30 天,付费账户可以更长。
第三是全球复制(这是 2025 年的重磅更新)。你的数据库主节点在一个区域,但读副本会自动同步到全球各地。用户在新加坡访问,数据就近从新加坡的副本读,延迟直接从三位数降到个位数。
但它也有硬限制
D1 不是万能的。它有几个你必须在选型前就了解的限制:
单数据库 10GB 上限。超过的话需要拆分,或者考虑其他方案。一个账户最多 50,000 个数据库——对大多数项目来说够用了,但如果你的业务是”每个用户一个数据库”这种模式,得算清楚了。
单写入者架构。同一时间只有一个节点能处理写操作,这意味着写入吞吐量有上限。实测大约 500-2000 writes/sec,和 PostgreSQL 的 10K-50K 没法比。如果你的业务是高频写入(比如实时竞价、日志管道),D1 可能扛不住。
顺序一致性,不是强一致性。这个后面会细说,简单讲就是你刚写入的数据,下一秒读可能看不到——但如果你用对了 Sessions API,这个问题可以完美解决。
说实话,D1 最适合的场景是那些读多写少的 Web 应用。大部分网站都是 90% 以上的读取操作,D1 的全球读复制能让这些请求从就近节点响应,体验提升是实打实的。
二、D1 架构解析:Durable Objects 与全球复制
这一章比较硬核,但如果你想把 D1 用好,这些概念绕不过去。
Durable Objects:每个数据库一个”管家”
D1 的核心是 Durable Objects。你可以把它想象成每个数据库都有一个专属的”管家进程”,这个管家负责:
- 保证全局唯一性:所有写操作都必须经过它,不会有两个人同时修改同一条数据导致的冲突
- 维护事务日志:每一次写入都被记录下来,用于故障恢复和副本同步
- 协调读写副本:告诉全球各地的副本”该更新了”
这个设计很巧妙。传统的分布式数据库需要在多个节点之间协调,网络延迟和节点故障都会导致各种问题。D1 的做法是:认准一个主节点,所有写操作都去那里排队,顺序处理,然后再异步同步到副本。
Snapshot Isolation:读操作不阻塞
当你在 D1 上执行一个 SELECT 查询时,它不会去主节点排队,而是直接从就近的副本读取一个”快照”。
什么意思?假设你的数据库在北京有个主节点,在东京、新加坡、悉尼都有副本。当东京的用户发起读请求时,D1 会路由到东京的副本,返回那个时间点的数据状态。这个快照是在查询开始的瞬间确定的,即使同一时刻主节点正在写入新数据,你的读操作也不会被阻塞。
但这里有个问题:如果你刚写了一条数据,马上读,可能读不到。因为副本还没同步过来。
这就是为什么 D1 提供了 Sessions API。
Lamport 时间戳:让顺序有意义
Leslie Lamport 在 1978 年提出了一种分布式系统的事件排序方法,后来被称为 Lamport 时间戳。核心思想很简单:每个事件都有一个逻辑时钟,后续事件的时间戳一定比前面的大。
D1 用这个机制来保证”顺序一致性”:如果你在一个会话里先写入再读取,D1 会确保你读到的是写入之后的最新数据,而不是某个还没同步的旧副本。
具体怎么做呢?每次写入完成后,D1 会返回一个”书签”(commit token)。这个书签就像一个标记点,告诉你”所有在这个点之前的修改都已经生效了”。你下次查询时带上这个书签,D1 就会确保你看到的数据至少比这个点新。
用户 → 写入订单 → 获得 commit token "abc123"
用户 → 查询订单(带 token "abc123")→ 确保看到刚写入的数据
全球复制的实现方式
当你创建一个 D1 数据库时,它会选择一个”主区域”(primary location)。默认是离你最近的 Cloudflare 数据中心,你也可以手动指定。
写入流程:
- 写请求到达就近的边缘节点
- 路由到主区域的 Durable Object
- 写入主数据库文件
- 异步复制到全球各区域的副本
读取流程:
- 读请求到达就近的边缘节点
- 从该区域的副本读取
- 如果是带 session 的读,确保一致性书签生效
Cloudflare 官方说全球复制不额外收费——这挺良心的,因为数据传输成本本来不小。但要注意,写操作还是会路由到主区域,延迟取决于你和主区域的物理距离。所以如果你把主区域设在美国,但大部分用户在亚洲,他们写入时会明显感觉到延迟。
三、Sessions API 实战:顺序一致性的代码实现
理论讲完了,来看看怎么写代码。
Sessions API 是 D1 在 2025 年推出的新特性,专门解决”写后读”一致性问题。如果你用过 MongoDB 的 causal consistency 或者 CockroachDB 的 follower reads,概念差不多——都是用某种标记来追踪因果关系。
最基本的用法
// 创建一个 Session
const session = env.DB.withSession();
// 普通的读查询,会路由到就近副本
const { results } = await session.prepare(
"SELECT * FROM products WHERE category = ?"
).bind("electronics").all();
// 写查询,自动路由到主库
await session.prepare(
"INSERT INTO orders (user_id, product_id, quantity) VALUES (?, ?, ?)"
).bind(userId, productId, 2).run();
// 获取当前 Session 的一致性书签
const bookmark = session.latestCommitToken;
这里的关键是 withSession()。它创建了一个”会话上下文”,在这个上下文里的所有操作都会共享同一个一致性视图。
三种一致性模式
Sessions API 提供了三种模式,对应不同的使用场景:
1. first-unconstrained(默认)
const session = env.DB.withSession("first-unconstrained");
最宽松的模式。读操作直接从就近副本读取,不管这个副本是不是最新的。适合那些对实时性要求不高的场景,比如商品列表、博客文章展示。
2. first-primary
const session = env.DB.withSession("first-primary");
第一次读操作会路由到主库,后续读操作走副本。这样能确保你读到的数据至少是创建 Session 时的最新状态。适合需要看到”刚刚写入的数据”但又不想每次都查主库的场景。
3. 使用书签继续之前的会话
// 从请求头获取上次的书签
const previousToken = request.headers.get("x-d1-token") ?? "first-unconstrained";
// 创建 Session,继续之前的会话
const session = env.DB.withSession(previousToken);
// 执行操作...
// 返回新的书签
response.headers.set("x-d1-token", session.latestCommitToken);
这是最强大的用法。你可以把书签存在客户端(比如浏览器 Cookie 或请求头),每次请求都带上,这样跨多个请求也能保持一致性。
实战场景:电商订单系统
假设你在做一个全球电商平台。用户浏览商品时,你希望从就近副本读取,延迟最低。但用户下单后查看订单,必须看到刚刚下的单。
export default {
async fetch(request, env) {
const url = new URL(request.url);
// 从请求头获取 session token(首次请求会是 null)
const token = request.headers.get("x-d1-token") ?? "first-unconstrained";
const session = env.DB.withSession(token);
// 情况1:浏览商品列表(不需要强一致性)
if (url.pathname === "/api/products") {
const { results } = await session.prepare(
"SELECT * FROM products WHERE status = ?"
).bind("active").all();
return new Response(JSON.stringify(results), {
headers: {
"Content-Type": "application/json",
"x-d1-token": session.latestCommitToken
}
});
}
// 情况2:创建订单(写操作,自动路由到主库)
if (url.pathname === "/api/orders" && request.method === "POST") {
const body = await request.json();
await session.prepare(`
INSERT INTO orders (user_id, total_amount, status)
VALUES (?, ?, ?)
`).bind(body.userId, body.total, "pending").run();
// 写入后立即查询,确保读到刚写入的数据
const order = await session.prepare(`
SELECT * FROM orders WHERE user_id = ?
ORDER BY created_at DESC LIMIT 1
`).bind(body.userId).first();
return new Response(JSON.stringify(order), {
headers: {
"Content-Type": "application/json",
"x-d1-token": session.latestCommitToken // 返回新 token
}
});
}
// 情况3:查看订单详情(使用之前的 token,确保一致性)
if (url.pathname.startsWith("/api/orders/")) {
const orderId = url.pathname.split("/")[3];
// 如果用户刚下过单,token 会确保读到最新数据
const order = await session.prepare(
"SELECT * FROM orders WHERE id = ?"
).bind(orderId).first();
return new Response(JSON.stringify(order), {
headers: {
"Content-Type": "application/json",
"x-d1-token": session.latestCommitToken
}
});
}
}
}
这个设计很实用。浏览商品时用 first-unconstrained,性能最好。下单后,客户端保存 token,后续查看订单时带上,保证一致性。
客户端怎么配合
前端需要做的很简单:把 x-d1-token 存起来,每次请求都带上。
// 前端示例
let d1Token = localStorage.getItem('d1-token') ?? 'first-unconstrained';
async function fetchProducts() {
const response = await fetch('/api/products', {
headers: { 'x-d1-token': d1Token }
});
d1Token = response.headers.get('x-d1-token');
localStorage.setItem('d1-token', d1Token);
return response.json();
}
async function createOrder(data) {
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-d1-token': d1Token
},
body: JSON.stringify(data)
});
d1Token = response.headers.get('x-d1-token');
localStorage.setItem('d1-token', d1Token);
return response.json();
}
你看,其实代码量不大,但解决的问题很大。没有这个机制,用户刚下完单去看订单列表,可能显示空空如也,体验很糟糕。
四、性能基准与竞品对比
数据不会骗人。这里我整理了几个主流方案的对比,数据来自官方文档和社区实测。
延迟对比
| 方案 | 读延迟 (p50) | 读延迟 (p99) | 写延迟 (p50) | 说明 |
|---|---|---|---|---|
| D1 | ~0.5ms | ~2-5ms | ~5-30ms | 读走边缘副本,写走主库 |
| Turso | ~0.02ms | ~0.1ms | ~15-50ms | 嵌入式读取,快得离谱 |
| PlanetScale | ~3-8ms | ~10-20ms | ~3-8ms | MySQL 兼容,读写都走代理 |
| PostgreSQL (Neon) | ~3-10ms | ~20-50ms | ~1-5ms | 传统架构,冷启动慢 |
几个观察:
Turso 的读延迟是真快。0.02 毫秒,基本上就是本地内存访问的速度。因为它用的是嵌入式 SQLite,数据库文件直接复制到你的边缘节点,读取完全本地化。但这是有代价的——数据同步需要额外处理,写入延迟反而更高。
D1 的读延迟也很优秀。0.5 毫秒对边缘数据库来说已经是顶级水平。但写延迟差距明显——因为写操作必须路由到主库,物理距离决定了延迟下限。如果你的主库在美西,用户在新加坡,写操作至少要跨太平洋,30 毫秒打底。
PlanetScale 和 Neon 更适合传统应用。它们的延迟数据看起来不如 D1 和 Turso 那么亮眼,但胜在生态成熟、功能完整。如果你需要复杂的 SQL 功能(存储过程、触发器、丰富的索引类型),这两个更合适。
吞吐量对比
| 方案 | 读吞吐 (QPS) | 写吞吐 (QPS) | 说明 |
|---|---|---|---|
| D1 | 10K-100K | 500-2K | 单数据库限制 |
| Turso | 无限(本地读) | 受同步限制 | 每个边缘节点独立读 |
| PlanetScale | 10K-50K | 5K-20K | 分片后可扩展 |
| PostgreSQL | 10K-100K | 10K-50K | 取决于实例规格 |
D1 的写吞吐量是短板。单写入者架构决定了上限。如果你的应用每秒需要写入 5000 条以上的数据,D1 可能会成为瓶颈。这时候要么考虑分库(但增加了复杂度),要么换其他方案。
免费额度对比
| 方案 | 存储空间 | 读取额度 | 写入额度 | 备注 |
|---|---|---|---|---|
| D1 | 5GB | 250亿行/月 | 5000万行/月 | 10GB/数据库上限 |
| Turso | 9GB | 10亿行/月 | 2500万行/月 | 含复制流量 |
| PlanetScale | 1GB | 100亿行/月 | 100亿行/月 | 无写入限制 |
| Neon | 0.5GB | 1亿单位/月 | 1亿单位/月 | 单位=读或写 |
从免费额度来看,D1 还是挺大方的。250 亿行读取,对于个人项目和小型应用绰绰有余。但要注意 D1 的写入限制——5000 万行/月,平均每天 166 万行。如果是日志收集、埋点上报这种高频写入场景,很容易超。
计费模式
D1 的付费模式很简单:按使用量计费,没有最低消费。超出免费额度后,每百万行读取 $0.001,每百万行写入 $0.10。存储每 GB $0.75/月。
Turso 的计费有点复杂,涉及”行读取”和”复制流量”两个维度,如果数据频繁更新,复制成本可能会很高。
PlanetScale 的计费基于”行读取”和”行写入”,写入成本比 D1 低,但读取成本稍高。
我的建议:如果你已经深度使用 Cloudflare 生态(Workers、KV、R2),D1 的计费集成会让账单更清晰。如果是独立项目,可以三家都试试,用实际数据决定。
五、选型决策树:什么时候选 D1
说了这么多,到底该不该选 D1?我画了个简单的决策树。
适合 D1 的场景
你的应用是读密集型的。比如内容网站、电商平台(浏览为主)、博客、文档系统。这类应用 90% 以上的操作都是读取,D1 的全球读复制能把延迟降到个位数毫秒。
你的用户分布在全球。传统的单区域数据库,远端用户每次请求都要跨洋过海。D1 让数据”走到用户身边”,体验提升立竿见影。
你已经在用 Cloudflare Workers。D1 和 Workers 的集成是原生的,配置几行代码就能用。不需要单独的数据库连接池,不用担心冷启动,开发体验很顺滑。
你的数据库规模在 10GB 以内。D1 的单库限制是 10GB,超过需要拆分。如果你的业务天生就是”每个租户一个数据库”,这个限制反而无所谓。
不适合 D1 的场景
高频写入应用。实时竞价系统、日志管道、IoT 数据收集——这些场景每秒可能产生数万次写入,D1 的单写入者架构会成为瓶颈。PostgreSQL、ClickHouse 或者 TimescaleDB 会更合适。
需要复杂事务。D1 目前支持 SQLite 的事务级别,如果你需要 SERIALIZABLE 隔离、跨数据库事务、或者复杂的存储过程,它满足不了。
数据量超过 10GB。虽然可以拆分,但增加了运维复杂度。如果你的数据天然就很大(比如时序数据、日志归档),不如一开始就选其他方案。
需要强一致性。D1 是最终一致性系统,写入后立即读取可能看不到最新数据(除非用 Sessions API)。如果你的业务要求任何时刻任何地点都能读到最新数据,得考虑其他方案。
从 PostgreSQL 迁移的注意事项
如果你想把现有的 PostgreSQL 应用迁移到 D1,有几件事需要提前考虑:
1. SQL 方言差异
SQLite 不支持 PostgreSQL 的某些特性:
- 没有
RETURNING子句(需要分两步:插入后查询) - 没有
SERIAL类型(用INTEGER PRIMARY KEY AUTOINCREMENT) - 没有
JSONB类型(用TEXT存储 JSON,用json_extract()函数) - 没有
ARRAY类型(需要用关联表)
2. 数据迁移工具
Cloudflare 官方提供了迁移工具,支持从 PostgreSQL 导出 SQL,然后导入 D1:
# 导出 PostgreSQL 数据
pg_dump --format=insert mydb > dump.sql
# 导入到 D1
npx wrangler d1 execute my-d1-database --file=dump.sql
但复杂的 schema 可能需要手动调整。
3. 连接方式变化
传统数据库是长连接,D1 是无状态的函数调用。你的 ORM 可能需要调整,或者直接用原生 SQL。Prisma 有 D1 的适配器,但功能还在完善中。
快速判断
如果你还是拿不准,试试这个简单的判断:
你的应用写入频率 > 1000 次/秒?
├─ 是 → 不选 D1
└─ 否
└─ 需要强一致性?
├─ 是 → 不选 D1(或用 Sessions API 配合)
└─ 否
└─ 数据量 > 10GB?
├─ 是 → 慎重考虑
└─ 否 → D1 很合适
总结
D1 的核心价值可以用三句话概括:边缘部署让延迟降到个位数毫秒,无服务器架构让你不用操心运维,Sessions API 用简单的方式解决了分布式系统最头疼的一致性问题。
但这不代表它适合所有场景。高频写入、复杂事务、超大规模数据——这些情况下,PostgreSQL 和专门的时序数据库仍然更合适。选型没有银弹,只有权衡。
如果你正在做一个全球用户、读多写少的 Web 应用,而且已经在用 Cloudflare Workers,那 D1 值得一试。创建一个测试数据库只需要几分钟:
# 创建数据库
npx wrangler d1 create my-first-db
# 创建表
npx wrangler d1 execute my-first-db --command="CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"
# 插入数据
npx wrangler d1 execute my-first-db --command="INSERT INTO users (name) VALUES ('test')"
实际跑一跑,感受一下从东京到美西数据库的延迟差异,你就知道它是不是适合你的项目了。
"D1 是 Cloudflare 的 SQLite 边缘数据库,提供全球读复制和无服务器体验。其 Sessions API 通过 Lamport 时间戳实现顺序一致性,解决了分布式系统常见的写后读一致性问题。"
参考资料
- Building D1: a Global Database — Cloudflare 官方博客,2024
- D1 Global Read Replication Beta — Cloudflare 官方博客,2025
- D1 Getting Started — Cloudflare 官方文档
- The SQLite Renaissance — DEV Community,2026
- Database Free Tier Comparison 2026 — Agent Deals
Cloudflare D1 数据库快速上手
从创建数据库到实现 Sessions API 一致性读取的完整流程
⏱️ 预计耗时: 15 分钟
- 1
步骤1: 创建 D1 数据库
使用 wrangler CLI 创建数据库:
```bash
npx wrangler d1 create my-first-db
```
创建后会返回 database_id,配置到 wrangler.toml:
```toml
[[d1_databases]]
binding = "DB"
database_name = "my-first-db"
database_id = "your-database-id"
``` - 2
步骤2: 创建数据表
执行 SQL 创建表结构:
```bash
npx wrangler d1 execute my-first-db --command="CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"
```
也可以使用 SQL 文件批量执行:
```bash
npx wrangler d1 execute my-first-db --file=./schema.sql
``` - 3
步骤3: 在 Worker 中使用 Sessions API
创建带会话的数据库连接,实现写后读一致性:
```typescript
export default {
async fetch(request, env) {
// 从请求头获取 session token
const token = request.headers.get("x-d1-token") ?? "first-unconstrained";
const session = env.DB.withSession(token);
// 写入数据
await session.prepare("INSERT INTO users (name) VALUES (?)")
.bind("test").run();
// 读取时确保一致性
const { results } = await session.prepare("SELECT * FROM users")
.all();
return new Response(JSON.stringify(results), {
headers: { "x-d1-token": session.latestCommitToken }
});
}
}
``` - 4
步骤4: 配置全球读复制
在 wrangler.toml 中指定主区域:
```toml
[[d1_databases]]
binding = "DB"
database_name = "my-first-db"
database_id = "your-database-id"
primary_location_hint = "apne1" # 东京区域
```
可选区域代码:
- apne1: 东京
- sfo1: 旧金山
- eur3: 法兰克福
常见问题
Cloudflare D1 和 Turso 有什么区别?
• D1:单写入者架构,写操作路由到主库,读取延迟约 0.5ms,适合读多写少场景
• Turso:嵌入式读取,延迟更低(约 0.02ms),但数据同步更复杂
D1 的优势是与 Cloudflare Workers 原生集成,免费额度更大(250 亿行读取/月);Turso 的优势是读取性能极致,适合对延迟敏感的场景。
D1 的 10GB 单库限制如何突破?
• 分库策略:按业务模块拆分,每个模块一个数据库
• 租户隔离:每个租户一个数据库,D1 支持最多 50,000 个数据库
• 混合存储:热数据放 D1,冷数据迁移到 R2 或其他对象存储
如果数据量持续增长超过 10GB,建议评估是否需要其他方案如 PlanetScale 或传统 PostgreSQL。
Sessions API 的三种模式该怎么选?
• first-unconstrained(默认):适合商品列表、博客展示等对实时性要求不高的场景
• first-primary:适合需要看到"刚写入数据"但又不想每次查主库的场景
• commit token 模式:适合电商下单、订单查看等需要跨请求一致性的场景
电商系统推荐:浏览时用 first-unconstrained,下单后保存 token,后续请求带上。
D1 适合高频写入场景吗?
如果你的应用有以下特征,建议选择其他方案:
• 实时竞价系统
• 日志管道、埋点上报
• IoT 数据收集
• 每秒写入超过 1000 次
这些场景更适合 PostgreSQL、ClickHouse 或 TimescaleDB。
从 PostgreSQL 迁移到 D1 需要注意什么?
• SQL 方言:SQLite 不支持 RETURNING、SERIAL、JSONB、ARRAY 类型
• 连接方式:从长连接变为无状态函数调用
• ORM 适配:Prisma 有 D1 适配器,但功能还在完善中
迁移步骤:
1. 使用 pg_dump 导出数据
2. 手动调整不兼容的 SQL 语法
3. 使用 wrangler d1 execute 导入
建议先用小规模数据测试,确认功能正常后再全量迁移。
D1 的全球复制会额外收费吗?
但需要注意:
• 写操作仍然路由到主库,延迟取决于你和主库的物理距离
• 如果主库在美国,用户在亚洲,写入延迟约 30ms+
• 读取免费额度很高(250 亿行/月),写入额度 5000 万行/月
建议将主库部署在用户最集中的区域,优化写入体验。
16 分钟阅读 · 发布于: 2026年5月5日 · 修改于: 2026年5月5日
相关文章
从免费到Enterprise:Cloudflare四大版本功能对比,5分钟搞懂什么时候该升级
从免费到Enterprise:Cloudflare四大版本功能对比,5分钟搞懂什么时候该升级
Cloudflare Pages部署静态博客完整指南:5个主流框架配置不再踩坑
Cloudflare Pages部署静态博客完整指南:5个主流框架配置不再踩坑
Cloudflare Pages 部署前端应用完全指南:React/Vue/Next.js 配置+报错解决

评论
使用 GitHub 账号登录后即可评论