Tao
Tao

从零开始:用 Cloudflare Durable Objects 构建你的第一个有状态 Serverless 应用

在 Serverless 的世界里,我们习惯了云函数的无状态(Stateless)特性。无论是 Cloudflare Workers 还是 AWS Lambda,它们都像记忆只有七秒的鱼,每次请求都是一次全新的开始。这在处理 API 请求、托管静态网站等场景下非常高效。

但一个问题随之而来:如果我需要在多次请求之间保持状态呢?

比如,你想做一个实时在线投票、一个简单的网页计数器,或者一个协作文档应用。传统的做法是引入外部数据库或 Redis 这样的缓存服务。这当然可行,但无疑增加了架构的复杂度、网络延迟和最终的成本。

有没有一种更简单的方式,让 Serverless 函数拥有“记忆”?

答案是肯定的。今天,我们就来认识 Cloudflare 专门为此打造的利器——Durable Objects (DO),并手把手带你构建一个全球分布式的实时计数器,亲身体验它的强大与优雅。

简单来说,Durable Objects 是一种有状态的 Serverless

你可以把它想象成一个存在于云端的、带持久化存储的数据库。与传统 Worker 在请求结束后就被销毁不同,Durable Object 只要被需要就会一直存在,并安全地保存着自己的状态。

为了更清晰地理解,我们来看一下它和传统 Worker 的核心区别:

特性 Cloudflare Worker (传统) Cloudflare Durable Object
状态 无状态 (Stateless) 有状态 (Stateful)
生命周期 请求结束即销毁 只要被需要就持续存在
一致性 最终一致性 强一致性 (单个对象内部)
并发模型 大规模并行 单线程(对象内部无并发冲突)
理想用途 API 网关、静态网站托管 聊天室、游戏状态、购物车、实时协作

Durable Objects 的魔法背后有三个关键角色:

  1. ID (DurableObjectId): 每个 DO 实例都有一个唯一的“身份证”。Cloudflare 会确保拥有相同 ID 的所有请求,都被路由到全球唯一的那个 DO 实例上。
  2. Stub: 这是存在于普通 Worker 中的“代理”或“句柄”。Worker 不能直接与 DO 对话,而是通过这个 Stub 来定位并向对应的 DO 实例发送消息。
  3. 单线程执行: 这是 DO 最优雅的设计之一。对于任意一个 DO 实例,它在同一时间只会处理一个请求。这就像一个只有一个窗口的银行柜台,所有请求都需要排队处理。这天然地解决了数据竞争(Race Condition)的问题,你不再需要担心并发写入导致的脏数据。

理论说完了,让我们动手实践吧!

我们将使用 Cloudflare 的命令行工具 wrangler 来创建一个项目,实现一个可以增加、减少和读取计数值的 API。

首先,请确保你已经安装了 Node.js 和 npm。然后,我们需要安装 wrangler 并登录你的 Cloudflare 账号。

bash

# 1. 安装 Wrangler CLI
npm install -g wrangler

# 2. 登录你的 Cloudflare 账户
wrangler login

创建一个新的 Worker 项目。

bash

wrangler init do-counter-app
cd do-counter-app

这是我们的核心业务逻辑。创建一个新文件 src/counter.js,并写入以下代码:

javascript

// src/counter.js

// 定义我们的 Durable Object 类
export class Counter {
  constructor(state, env) {
    this.state = state;
  }

  // 处理传入的请求
  async fetch(request) {
    // 解析 URL 来判断用户的意图
    const url = new URL(request.url);
    let value = (await this.state.storage.get("value")) || 0;

    switch (url.pathname) {
      case "/increment":
        value++;
        break;
      case "/decrement":
        value--;
        break;
      case "/":
        // 什么都不做,只读取当前值
        break;
      default:
        return new Response("Not found", { status: 404 });
    }

    // 将更新后的值写回持久化存储
    await this.state.storage.put("value", value);

    // 返回当前值
    return new Response(value);
  }
}

代码讲解:

  • constructor(state, env): 构造函数接收一个 state 对象,state.storage 就是我们用来读写持久化数据的 API。
  • fetch(request): 这是 DO 的入口点,和普通 Worker 一样。我们通过解析请求的 URL 路径(如 /increment)来决定执行什么操作。
  • this.state.storage.get("value"): 从 DO 的私有存储中异步读取名为 “value” 的键。如果不存在,默认为 0。
  • this.state.storage.put("value", value): 将新的计数值异步写回存储。这个操作是原子性的,并且会被持久化。

这是将我们的代码和 Cloudflare 平台连接起来的关键一步。打开根目录下的 wrangler.toml 文件,修改成如下内容:

toml

name = "do-counter-app"
main = "src/index.js"
compatibility_date = "2023-10-30" # 请使用较新的日期

# 关键部分:声明并绑定 Durable Object
[durable_objects]
bindings = [
  { name = "COUNTER", class_name = "Counter" }
]

[[migrations]]
tag = "v1" # 必须的,用于标识此次绑定
new_classes = ["Counter"] # 将我们的 Counter 类加入迁移

配置讲解:

  • [durable_objects] 部分声明了我们要使用的 DO。
  • name = "COUNTER": 这是我们在 Worker 代码中用来访问这个 DO 的绑定名称
  • class_name = "Counter": 这指向我们在 src/counter.js 中导出的那个类的名字。
  • [[migrations]]: 每次你新增或移除一个 DO 类时,都需要添加一个 migration。这是 Cloudflare 用来管理 DO 类变更的方式。

现在,我们需要编写入口 Worker (src/index.js),它将作为用户请求的门户,负责将请求转发给我们的 Counter DO。

修改 src/index.js 文件内容如下:

javascript

// src/index.js

// 导入我们的 Durable Object 类,这样 Wrangler 才能找到它
export { Counter } from './counter.js';

export default {
  async fetch(request, env) {
    // 我们需要一个方法来获取一个特定且唯一的 DO 实例。
    // 这里我们使用一个固定的名字 "my-counter" 来确保所有请求都打到同一个计数器实例上。
    // `idFromName` 会根据名字生成一个唯一的 ID。
    let id = env.COUNTER.idFromName("my-counter");

    // `get` 方法使用这个 ID 来获取 DO 的 "Stub" (代理)。
    let stub = env.COUNTER.get(id);

    // 将用户的原始请求直接转发给获取到的 DO 实例。
    // DO 实例会执行我们之前编写的 fetch 方法。
    return await stub.fetch(request);
  },
};

代码讲解:

  • export { Counter } from './counter.js';: 这行很重要,它让 wrangler 在部署时能够识别并打包我们的 DO 类。
  • env.COUNTER: env 对象包含了我们在 wrangler.toml 中定义的绑定。COUNTER 就是我们定义的名字。
  • env.COUNTER.idFromName("my-counter"): 我们通过一个固定的字符串 “my-counter” 来获取一个确定性的、唯一的 DO ID。这意味着所有使用这个名字的请求都会被路由到同一个 DO 实例。
  • env.COUNTER.get(id): 通过 ID 获取到 DO 的 Stub。
  • stub.fetch(request): Worker 将原始请求转发给 DO 处理,并将其响应返回给用户。

一切准备就绪!在你的项目根目录下运行部署命令:

bash

wrangler deploy

部署成功后,wrangler 会告诉你一个 URL,例如 https://do-counter-app.<your-subdomain>.workers.dev

现在,打开你的终端,使用 curl 来测试它:

  1. 增加计数器:

    bash

    curl https://do-counter-app.<your-subdomain>.workers.dev/increment
    # 预期返回: 1
  2. 再次增加:

    bash

    curl https://do-counter-app.<your-subdomain>.workers.dev/increment
    # 预期返回: 2
  3. 获取当前值:

    bash

    curl https://do-counter-app.<your-subdomain>.workers.dev/
    # 预期返回: 2
  4. 减少计数器:

    bash

    curl https://do-counter-app.<your-subdomain>.workers.dev/decrement
    # 预期返回: 1

你会发现,无论你发送多少次请求,间隔多久,计数值都被正确地“记住”了。你刚刚成功地构建并部署了你的第一个有状态 Serverless 应用!

通过这个简单的计数器,我们学习了 Durable Objects 的核心思想:将状态和计算绑定在一起,并通过唯一的 ID 进行全局访问。它完美地解决了 Serverless 的状态管理难题,同时通过单线程模型保证了数据的一致性。

现在你已经掌握了基础,不妨思考一下还能用它来做什么?

  • 一个简单的在线投票系统(使用不同的投票主题作为 idFromName 的参数)。
  • 一个记录 API 调用次数的速率限制器。
  • 一个简单的 WebSocket 聊天室(是的,Durable Objects 对 WebSockets 有一流的支持!)。

Durable Objects 为 Serverless 架构打开了一扇新的大门。当你下一次需要为你的 Worker 添加“记忆”时,希望你能想起它。

相关内容