从零开始:用 Cloudflare Durable Objects 构建你的第一个有状态 Serverless 应用
在 Serverless 的世界里,我们习惯了云函数的无状态(Stateless)特性。无论是 Cloudflare Workers 还是 AWS Lambda,它们都像记忆只有七秒的鱼,每次请求都是一次全新的开始。这在处理 API 请求、托管静态网站等场景下非常高效。
但一个问题随之而来:如果我需要在多次请求之间保持状态呢?
比如,你想做一个实时在线投票、一个简单的网页计数器,或者一个协作文档应用。传统的做法是引入外部数据库或 Redis 这样的缓存服务。这当然可行,但无疑增加了架构的复杂度、网络延迟和最终的成本。
有没有一种更简单的方式,让 Serverless 函数拥有“记忆”?
答案是肯定的。今天,我们就来认识 Cloudflare 专门为此打造的利器——Durable Objects (DO),并手把手带你构建一个全球分布式的实时计数器,亲身体验它的强大与优雅。
什么是 Durable Objects?
简单来说,Durable Objects 是一种有状态的 Serverless。
你可以把它想象成一个存在于云端的、带持久化存储的数据库。与传统 Worker 在请求结束后就被销毁不同,Durable Object 只要被需要就会一直存在,并安全地保存着自己的状态。
为了更清晰地理解,我们来看一下它和传统 Worker 的核心区别:
特性 | Cloudflare Worker (传统) | Cloudflare Durable Object |
---|---|---|
状态 | 无状态 (Stateless) | 有状态 (Stateful) |
生命周期 | 请求结束即销毁 | 只要被需要就持续存在 |
一致性 | 最终一致性 | 强一致性 (单个对象内部) |
并发模型 | 大规模并行 | 单线程(对象内部无并发冲突) |
理想用途 | API 网关、静态网站托管 | 聊天室、游戏状态、购物车、实时协作 |
核心工作机制
Durable Objects 的魔法背后有三个关键角色:
- ID (
DurableObjectId
): 每个 DO 实例都有一个唯一的“身份证”。Cloudflare 会确保拥有相同 ID 的所有请求,都被路由到全球唯一的那个 DO 实例上。 - Stub: 这是存在于普通 Worker 中的“代理”或“句柄”。Worker 不能直接与 DO 对话,而是通过这个 Stub 来定位并向对应的 DO 实例发送消息。
- 单线程执行: 这是 DO 最优雅的设计之一。对于任意一个 DO 实例,它在同一时间只会处理一个请求。这就像一个只有一个窗口的银行柜台,所有请求都需要排队处理。这天然地解决了数据竞争(Race Condition)的问题,你不再需要担心并发写入导致的脏数据。
理论说完了,让我们动手实践吧!
实战演练:构建一个全球实时计数器
我们将使用 Cloudflare 的命令行工具 wrangler
来创建一个项目,实现一个可以增加、减少和读取计数值的 API。
准备工作
首先,请确保你已经安装了 Node.js 和 npm。然后,我们需要安装 wrangler
并登录你的 Cloudflare 账号。
# 1. 安装 Wrangler CLI
npm install -g wrangler
# 2. 登录你的 Cloudflare 账户
wrangler login
步骤 1: 初始化项目
创建一个新的 Worker 项目。
wrangler init do-counter-app
cd do-counter-app
步骤 2: 编写 Durable Object 类
这是我们的核心业务逻辑。创建一个新文件 src/counter.js
,并写入以下代码:
// 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)
: 将新的计数值异步写回存储。这个操作是原子性的,并且会被持久化。
步骤 3: 配置 wrangler.toml
这是将我们的代码和 Cloudflare 平台连接起来的关键一步。打开根目录下的 wrangler.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 类变更的方式。
步骤 4: 编写主 Worker
现在,我们需要编写入口 Worker (src/index.js
),它将作为用户请求的门户,负责将请求转发给我们的 Counter
DO。
修改 src/index.js
文件内容如下:
// 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 处理,并将其响应返回给用户。
步骤 5: 部署和测试
一切准备就绪!在你的项目根目录下运行部署命令:
wrangler deploy
部署成功后,wrangler
会告诉你一个 URL,例如 https://do-counter-app.<your-subdomain>.workers.dev
。
现在,打开你的终端,使用 curl
来测试它:
-
增加计数器:
curl https://do-counter-app.<your-subdomain>.workers.dev/increment # 预期返回: 1
-
再次增加:
curl https://do-counter-app.<your-subdomain>.workers.dev/increment # 预期返回: 2
-
获取当前值:
curl https://do-counter-app.<your-subdomain>.workers.dev/ # 预期返回: 2
-
减少计数器:
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 添加“记忆”时,希望你能想起它。