Cloudflare Workers Durable Objects
普通 Worker 是无状态的,每个请求都可能落在不同的节点,代码里的变量在请求之间无法共享。需要跨请求协调状态的场景,比如计数器、聊天室、协同编辑,就要用到 Durable Objects。这是 Workers 体系里最难理解的一块,值得单独一篇讲透。
无状态的问题
先看一个典型问题。想用普通 Worker 实现一个访问计数器,最直觉的写法是
let count = 0; // 存在哪?每个节点一份,互不相同
export default {
async fetch(request, env, ctx) {
count++;
return Response.json({ count });
},
};
这个写法有两个致命问题。第一,代码可能被复制到多个节点运行,每个节点都有自己的 count,不同用户访问看到的数字不一样。第二,节点空闲后资源被回收,变量清零,计数全部丢失。
用 KV 可以解决持久化,但 KV 的最终一致性又带来新问题。两个请求同时读旧值再加 1 写回,就丢失了一次计数。
Durable Objects 就是为这类”需要单点协调的状态”而生的。
DO 的核心概念
Durable Objects(简称 DO)可以理解为”有状态且带持久化存储的 Worker 实例”,三个核心特性
| 特性 | 说明 |
|---|---|
| 唯一 ID | 每个 DO 实例有一个唯一 ID,同一 ID 的请求永远路由到同一实例 |
| 单线程执行 | 一个实例同一时间只处理一个请求,天然避免并发竞争 |
| 内置持久存储 | 实例自带 SQLite 存储,数据不随资源回收丢失 |
用聊天室打比方。一个房间就是一个 DO 实例,房间号就是 ID,所有人都进同一个房间,房间里的数据(消息、成员)由房间自己管理,不会散落在不同节点。
配置绑定
DO 类和普通 Worker 一样写在项目里,需要两步配置
// src/index.js
export class Counter {
constructor(state, env) {
this.state = state;
}
// 这个实例收到请求时调用
async fetch(request) {
const key = "count";
let value = (await this.state.storage.get(key)) || 0;
value++;
await this.state.storage.put(key, value);
return Response.json({ count: value });
}
}
wrangler.toml 中注册这个类
name = "my-worker"
main = "src/index.js"
compatibility_date = "2026-01-01"
[durable_objects]
bindings = [{ name = "COUNTER", class_name = "Counter" }]
[[migrations]]
tag = "v1"
new_sqlite_classes = ["Counter"]
durable_objects 里的 bindings 把代码中的类暴露给 Worker,migrations 通知平台创建这个类的实例存储。以后新增 DO 类时,要追加一条 migrations 记录。
获取 DO 实例
Worker 里通过绑定拿到 DO 实例的引用,转发请求过去
export default {
async fetch(request, env, ctx) {
// 根据名称获取稳定 ID
const id = env.COUNTER.idFromName("my-counter");
// 根据 ID 获取实例引用(stub)
const stub = env.COUNTER.get(id);
// 转发请求给 DO 实例执行
return stub.fetch(request);
},
};
三种获取 ID 的方式
| 方法 | 用途 |
|---|---|
idFromName(name) |
同一名称始终得到同一 ID,适合”每个房间一个” |
newUniqueId() |
每次生成全新 ID,适合”每次会话一个” |
idFromString(id) |
从已有 ID 字符串解析,配合持久化使用 |
stub.fetch() 的用法和普通 fetch 完全一致,DO 实例的 fetch 方法会收到这个请求并处理。
存储 API
DO 实例用 this.state.storage 读写持久化数据,接口风格和 KV 类似
// 读取
const value = await this.state.storage.get("key");
// 写入
await this.state.storage.put("key", value);
// 删除
await this.state.storage.delete("key");
// 列出
const items = await this.state.storage.list();
和 KV 最大的区别是,DO 的存储是强一致的,写入后立刻能读到新值,因为它只有一份,就在这个实例自己身上。
单线程的威力
一个 DO 实例同一时刻只处理一个请求,这是理解 DO 的关键。再回到计数器场景
请求 A 进入实例,读取 count=1,加 1 写入 count=2,响应完成
请求 B 排队等待,A 完成后进入,读取 count=2,加 1 写入 count=3
两个请求不可能同时修改数据,竞争问题从机制上被消除了。代价是同一实例的请求只能串行处理,单实例吞吐有上限,高并发场景需要把用户分散到多个实例(比如按用户 ID 分房间)。
适用场景
| 场景 | 用法 |
|---|---|
| 计数器 | 每类数据一个实例,见上文示例 |
| 聊天室 | 每个房间一个实例,见下一篇 WebSocket 实战 |
| 会话管理 | 每个用户一个实例,存储登录态 |
| 分布式锁 | 用单线程特性实现互斥 |
| 任务协调 | 多个 Worker 协作时的状态协调 |
不适用的是简单读写类数据,KV 和 D1 更便宜也更简单,只有”需要并发协调”时才上 DO。
限制与计费
| 项目 | 说明 |
|---|---|
| 免费计划 | 每天 10 万次请求,使用 SQLite 存储后端 |
| 单实例内存 | 128MB |
| 请求时长 | 连接保持期间无硬上限 |
| 密钥管理 | 和普通 Worker 相同,走 secrets |
DO 是 Workers 从”无状态计算”走向”有状态应用”的桥梁。下一篇用 DO 实现 WebSocket 实时通信,是 DO 最典型的应用,也是 Workers 的进阶难点。
上一篇 Cloudflare Workers 调试与线上部署
下一篇 Cloudflare Workers WebSocket