切换语言
切换主题

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。你可以把它想象成每个数据库都有一个专属的”管家进程”,这个管家负责:

  1. 保证全局唯一性:所有写操作都必须经过它,不会有两个人同时修改同一条数据导致的冲突
  2. 维护事务日志:每一次写入都被记录下来,用于故障恢复和副本同步
  3. 协调读写副本:告诉全球各地的副本”该更新了”

这个设计很巧妙。传统的分布式数据库需要在多个节点之间协调,网络延迟和节点故障都会导致各种问题。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 数据中心,你也可以手动指定。

写入流程:

  1. 写请求到达就近的边缘节点
  2. 路由到主区域的 Durable Object
  3. 写入主数据库文件
  4. 异步复制到全球各区域的副本

读取流程:

  1. 读请求到达就近的边缘节点
  2. 从该区域的副本读取
  3. 如果是带 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-8msMySQL 兼容,读写都走代理
PostgreSQL (Neon)~3-10ms~20-50ms~1-5ms传统架构,冷启动慢
0.5ms
D1 读延迟
p50,边缘副本
0.02ms
Turso 读延迟
嵌入式读取
500-2K
D1 写吞吐
writes/sec
10GB
单库上限
超过需拆分
数据来源: 官方文档与社区实测

几个观察:

Turso 的读延迟是真快。0.02 毫秒,基本上就是本地内存访问的速度。因为它用的是嵌入式 SQLite,数据库文件直接复制到你的边缘节点,读取完全本地化。但这是有代价的——数据同步需要额外处理,写入延迟反而更高。

D1 的读延迟也很优秀。0.5 毫秒对边缘数据库来说已经是顶级水平。但写延迟差距明显——因为写操作必须路由到主库,物理距离决定了延迟下限。如果你的主库在美西,用户在新加坡,写操作至少要跨太平洋,30 毫秒打底。

PlanetScale 和 Neon 更适合传统应用。它们的延迟数据看起来不如 D1 和 Turso 那么亮眼,但胜在生态成熟、功能完整。如果你需要复杂的 SQL 功能(存储过程、触发器、丰富的索引类型),这两个更合适。

吞吐量对比

方案读吞吐 (QPS)写吞吐 (QPS)说明
D110K-100K500-2K单数据库限制
Turso无限(本地读)受同步限制每个边缘节点独立读
PlanetScale10K-50K5K-20K分片后可扩展
PostgreSQL10K-100K10K-50K取决于实例规格

D1 的写吞吐量是短板。单写入者架构决定了上限。如果你的应用每秒需要写入 5000 条以上的数据,D1 可能会成为瓶颈。这时候要么考虑分库(但增加了复杂度),要么换其他方案。

免费额度对比

方案存储空间读取额度写入额度备注
D15GB250亿行/月5000万行/月10GB/数据库上限
Turso9GB10亿行/月2500万行/月含复制流量
PlanetScale1GB100亿行/月100亿行/月无写入限制
Neon0.5GB1亿单位/月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 时间戳实现顺序一致性,解决了分布式系统常见的写后读一致性问题。"

参考资料

Cloudflare D1 数据库快速上手

从创建数据库到实现 Sessions API 一致性读取的完整流程

⏱️ 预计耗时: 15 分钟

  1. 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

    步骤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

    步骤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

    步骤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 有什么区别?
两者都是边缘 SQLite 数据库,但架构不同:

• 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 适合高频写入场景吗?
不太适合。D1 的单写入者架构导致写入吞吐量上限约 500-2000 writes/sec,远低于 PostgreSQL 的 10K-50K。

如果你的应用有以下特征,建议选择其他方案:
• 实时竞价系统
• 日志管道、埋点上报
• 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 的全球复制会额外收费吗?
不会。Cloudflare 官方明确表示全球读复制不额外收费,数据传输成本已包含在计费中。

但需要注意:
• 写操作仍然路由到主库,延迟取决于你和主库的物理距离
• 如果主库在美国,用户在亚洲,写入延迟约 30ms+
• 读取免费额度很高(250 亿行/月),写入额度 5000 万行/月

建议将主库部署在用户最集中的区域,优化写入体验。

16 分钟阅读 · 发布于: 2026年5月5日 · 修改于: 2026年5月5日

当前属于系列阅读 第 21 / 21 篇

Cloudflare 全家桶实战

如果你是从搜索进入这篇文章,建议顺手补上上一篇或继续下一篇,这样更容易把同一主题读完整。

查看系列总览

相关文章

BetterLink

想持续收到这个主题的更新?

你可以直接关注作者更新、订阅 RSS,或者继续沿着系列入口往下读,避免下次又回到搜索结果重新找。

关注公众号

评论

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