Cloudflare Workers 让我们能在全球 300+ 个边缘节点运行无状态代码,但当你需要状态——比如实时协作编辑器的文档状态、聊天室的在线用户列表、或者分布式限流器的计数器——纯无状态的 Worker 就力不从心了。Durable Objects(持久对象)正是为解决这个问题而生的边缘原语,它提供了一个全球唯一、单实例运行、自带持久化存储的 Actor,让你在边缘优雅地管理有状态逻辑。
截至目前,Durable Objects 已经从一个实验性功能演进为 Cloudflare 平台上的一等公民,支撑了大量生产级应用。但很多开发者对它的理解还停留在「边缘数据库」的层面——实际上,它的设计哲学和使用模式远比这深刻。
🏗️ 一、Durable Objects 核心概念
1.1 Actor 模型:为什么需要单实例?
传统的无服务器架构中,每个请求可能被调度到不同的实例,多个实例并发处理同一个资源时,你必须依赖外部存储(Redis、数据库)来做状态同步和并发控制。这不仅增加了延迟,还引入了分布式一致性难题。
Durable Objects 采用经典的 Actor 模型:每个 Object 实例在全球范围内只有一个活跃副本,所有对该 Object 的请求都会被路由到同一个实例。这意味着:
- ✅ 无需锁机制 — 单线程执行,天然无竞态条件
- ✅ 内存状态即真实状态 — 实例内存中的数据就是最新的
- ✅ 自动持久化 — 结合内置 Storage API,状态不会丢失
- ❌ 不适合高吞吐写入 — 单实例是瓶颈,超过 ~1000 req/s 需要考虑分片
// ❌ 传统方式:无状态 Worker + Redis,需要处理并发
export default {
async fetch(request, env) {
const count = await env.REDIS.incr('rate:counter');
if (count > 100) return new Response('Rate limited', { status: 429 });
// 但这里的 incr 和后续操作不是原子的!
return new Response('OK');
}
};
// ✅ Durable Objects 方式:单实例保证,天然原子
export class RateLimiter {
constructor(state, env) {
this.state = state;
this.count = 0;
this.windowStart = Date.now();
}
async fetch(request) {
const now = Date.now();
if (now - this.windowStart > 60000) {
this.count = 0;
this.windowStart = now;
}
this.count++;
if (this.count > 100) {
return new Response(JSON.stringify({ allowed: false, remaining: 0 }), {
status: 429,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify({
allowed: true,
remaining: 100 - this.count
}), { headers: { 'Content-Type': 'application/json' } });
}
}
1.2 生命周期:实例何时创建、何时销毁?
理解 Durable Objects 的生命周期是避免踩坑的关键:
| 阶段 | 触发条件 | 行为 |
|---|---|---|
| 创建 | 首次通过 idFromName() 或 newUniqueId() 访问 |
分配全球唯一 ID,在最近的边缘数据中心启动实例 |
| 活跃 | 有请求到达 | 实例保持在内存中,处理请求 |
| 休眠 | ~10 秒无请求 | 实例被卸载,内存状态丢失(持久化数据保留) |
| 唤醒 | 新请求到达 | 从持久化存储恢复状态,重新启动实例 |
| 销毁 | 通过 deleteAll() 或过期策略 |
清除所有持久化数据,不可恢复 |
⚠️ **警告:**实例休眠时内存中的临时状态会丢失。不要把重要数据只放在内存中——始终配合 Storage API 使用。
1.3 ID 策略:命名 ID vs 唯一 ID
// 入口 Worker 中创建 Durable Object 引用
export default {
async fetch(request, env) {
const url = new URL(request.url);
// 命名 ID:相同名称总是路由到同一个实例
// 适用于:用户级对象、文档级对象、房间级对象
const userId = request.headers.get('X-User-Id');
const userObjId = env.USER_STATE.idFromName(`user:${userId}`);
const userObj = env.USER_STATE.get(userObjId);
// 唯一 ID:每次调用生成新的唯一实例
// 适用于:临时会话、一次性任务
const sessionObjId = env.SESSION.newUniqueId();
const sessionObj = env.SESSION.get(sessionObjId);
return userObj.fetch(request);
}
};
💡 **提示:**命名 ID 是确定性的——
idFromName("user:123")在任何数据中心都会得到相同的 ID。利用这个特性可以实现全球一致的用户状态。
🔥 二、三大实战场景
2.1 场景一:实时协作编辑器
这是 Durable Objects 最经典的用例。每个文档对应一个 Durable Object 实例,管理文档内容和连接到该文档的所有客户端的 WebSocket。
// document-do.js
export class DocumentEditor {
constructor(state, env) {
this.state = state;
this.env = env;
this.sessions = []; // WebSocket 连接
this.document = ''; // 文档内容
// 从持久化存储恢复文档
this.state.blockConcurrencyWhile(async () => {
const saved = await this.state.storage.get('document');
if (saved) this.document = saved;
});
}
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/websocket') {
// 升级为 WebSocket
const pair = new WebSocketPair();
const [client, server] = [pair[0], pair[1]];
this.state.acceptWebSocket(server);
this.sessions.push(server);
// 发送当前文档内容给新连接的客户端
server.send(JSON.stringify({
type: 'init',
content: this.document,
users: this.sessions.length
}));
// 广播用户数变化
this.broadcast({ type: 'users', count: this.sessions.length });
return new Response(null, { status: 101, webSocket: client });
}
if (url.pathname === '/content') {
return new Response(this.document, {
headers: { 'Content-Type': 'text/plain' }
});
}
return new Response('Not found', { status: 404 });
}
// WebSocket 消息处理
async webSocketMessage(ws, message) {
try {
const data = JSON.parse(message);
if (data.type === 'edit') {
// 应用编辑操作(这里简化为全量替换)
this.document = data.content;
// 持久化到存储
await this.state.storage.put('document', this.document);
// 广播给其他客户端(排除发送者)
this.broadcast({ type: 'update', content: this.document }, ws);
}
} catch (e) {
ws.send(JSON.stringify({ type: 'error', message: e.message }));
}
}
// WebSocket 关闭处理
async webSocketClose(ws, code, reason) {
this.sessions = this.sessions.filter(s => s !== ws);
this.broadcast({ type: 'users', count: this.sessions.length });
}
broadcast(message, exclude) {
const payload = JSON.stringify(message);
for (const ws of this.sessions) {
if (ws !== exclude) {
try { ws.send(payload); } catch (e) { /* 忽略已断开的连接 */ }
}
}
}
}
📌 **记住:**Durable Objects 的 WebSocket 支持 Hibernation 模式——当没有消息时,实例可以休眠,不消耗 CPU 时间。这对长连接场景(聊天室、协作编辑器)的成本优化至关重要。
2.2 场景二:分布式限流器(滑动窗口)
传统限流器要么用 Redis(增加延迟),要么用本地内存(不准确)。Durable Objects 提供了一个既准确又低延迟的方案。
// rate-limiter-do.js
export class SlidingWindowRateLimiter {
constructor(state, env) {
this.state = state;
this.windowMs = 60000; // 1 分钟窗口
this.maxRequests = 100;
this.state.blockConcurrencyWhile(async () => {
const saved = await this.state.storage.get('requests');
this.requests = saved || [];
});
}
async fetch(request) {
const now = Date.now();
const windowStart = now - this.windowMs;
// 清理过期请求记录
this.requests = this.requests.filter(ts => ts > windowStart);
const currentCount = this.requests.length;
const allowed = currentCount < this.maxRequests;
if (allowed) {
this.requests.push(now);
// 持久化(异步,不阻塞响应)
this.state.storage.put('requests', this.requests);
}
// 计算窗口重置时间
const resetTime = this.requests.length > 0
? this.requests[0] + this.windowMs
: now + this.windowMs;
const headers = {
'Content-Type': 'application/json',
'X-RateLimit-Limit': String(this.maxRequests),
'X-RateLimit-Remaining': String(Math.max(0, this.maxRequests - this.requests.length)),
'X-RateLimit-Reset': String(Math.ceil(resetTime / 1000)),
};
if (!allowed) {
const retryAfter = Math.ceil((resetTime - now) / 1000);
headers['Retry-After'] = String(retryAfter);
return new Response(JSON.stringify({
error: 'Too many requests',
retryAfter
}), { status: 429, headers });
}
return new Response(JSON.stringify({
allowed: true,
remaining: this.maxRequests - this.requests.length
}), { headers });
}
}
性能对比:
| 方案 | 延迟 | 准确性 | 全球一致性 | 成本 |
|---|---|---|---|---|
| Redis + Lua 脚本 | 1-5ms(同区域) | 高 | 需要 Redis 集群 | 中 |
| 本地内存 | <0.1ms | 低(多实例不一致) | ❌ | 低 |
| Durable Objects | <1ms(边缘最近) | 高 | ✅ 全球唯一实例 | 按请求计费 |
| Cloudflare Rate Limiting | <0.5ms | 高 | ✅ | 免费额度有限 |
2.3 场景三:轻量级任务队列
Durable Objects 可以作为边缘任务队列,处理不需要持久化到传统消息队列的轻量异步任务。
// task-queue-do.js
export class TaskQueue {
constructor(state, env) {
this.state = state;
this.tasks = [];
this.processing = false;
this.state.blockConcurrencyWhile(async () => {
this.tasks = (await this.state.storage.get('tasks')) || [];
});
}
async fetch(request) {
const url = new URL(request.url);
if (request.method === 'POST' && url.pathname === '/enqueue') {
const task = await request.json();
task.id = crypto.randomUUID();
task.status = 'pending';
task.createdAt = Date.now();
this.tasks.push(task);
await this.state.storage.put('tasks', this.tasks);
// 如果没有在处理,启动处理循环
if (!this.processing) {
this.processQueue();
}
return new Response(JSON.stringify({ queued: true, taskId: task.id }), {
headers: { 'Content-Type': 'application/json' }
});
}
if (url.pathname === '/status') {
return new Response(JSON.stringify({
pending: this.tasks.filter(t => t.status === 'pending').length,
total: this.tasks.length
}), { headers: { 'Content-Type': 'application/json' } });
}
return new Response('Not found', { status: 404 });
}
async processQueue() {
this.processing = true;
while (this.tasks.some(t => t.status === 'pending')) {
const task = this.tasks.find(t => t.status === 'pending');
if (!task) break;
task.status = 'processing';
await this.state.storage.put('tasks', this.tasks);
try {
// 执行任务(示例:发送 webhook)
await this.executeTask(task);
task.status = 'completed';
} catch (e) {
task.status = 'failed';
task.error = e.message;
task.retries = (task.retries || 0) + 1;
// 重试逻辑
if (task.retries < 3) {
task.status = 'pending';
}
}
await this.state.storage.put('tasks', this.tasks);
}
this.processing = false;
}
async executeTask(task) {
// 根据任务类型执行不同逻辑
switch (task.type) {
case 'webhook':
await fetch(task.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task.payload)
});
break;
case 'cleanup':
// 清理过期数据
const cutoff = Date.now() - (task.maxAge || 86400000);
this.tasks = this.tasks.filter(t =>
t.status === 'completed' && t.createdAt > cutoff
);
break;
default:
throw new Error(`Unknown task type: ${task.type}`);
}
}
}
⚙️ 三、生产环境最佳实践
3.1 wrangler.toml 配置
# wrangler.toml
name = "my-app"
main = "src/worker.js"
compatibility_date = "2026-01-01"
# Durable Objects 绑定
[[durable_objects.bindings]]
name = "DOCUMENT"
class_name = "DocumentEditor"
[[durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "SlidingWindowRateLimiter"
[[durable_objects.bindings]]
name = "TASK_QUEUE"
class_name = "TaskQueue"
# Durable Objects 迁移(版本管理)
[[migrations]]
tag = "v1"
new_classes = ["DocumentEditor", "SlidingWindowRateLimiter", "TaskQueue"]
# 存储配置
[vars]
ENVIRONMENT = "production"
3.2 成本模型与容量规划
Durable Objects 的计费包含三部分:
| 计费项 | 免费额度 | 超出价格 | 说明 |
|---|---|---|---|
| 请求次数 | 100 万次/月 | $0.15 / 百万次 | 每次 fetch() 调用计一次 |
| 持久化存储写入 | 1 GB·小时 | $0.20 / GB·小时 | 存储在 Object 内部的数据 |
| 持久化存储读取 | 1 GB·小时 | $0.20 / GB·小时 | 读取存储的数据 |
| WebSocket 消息 | 100 万条/月 | $0.08 / 百万条 | 通过 WebSocket 发送的消息 |
⚠️ **警告:**Durable Objects 的请求费用比普通 Workers 高约 10 倍。不要把所有逻辑都放到 DO 中——只把需要状态管理的部分放在 DO 里,无状态逻辑仍然用普通 Worker 处理。
3.3 Alarm API:定时任务
Durable Objects 支持 Alarm API,可以在指定时间唤醒实例执行任务,无需外部 Cron 服务。
// alarm-do.js
export class ScheduledTask {
constructor(state, env) {
this.state = state;
}
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/schedule') {
const { delayMs, taskData } = await request.json();
const alarmTime = Date.now() + delayMs;
// 设置 alarm(每个 DO 只能有一个活跃 alarm)
await this.state.storage.put('taskData', taskData);
await this.state.storage.setAlarm(alarmTime);
return new Response(JSON.stringify({
scheduled: true,
executeAt: new Date(alarmTime).toISOString()
}));
}
return new Response('OK');
}
// alarm 触发时调用
async alarm() {
const taskData = await this.state.storage.get('taskData');
if (!taskData) return;
try {
// 执行定时任务
await fetch(taskData.callbackUrl, {
method: 'POST',
body: JSON.stringify(taskData)
});
// 如果需要重复执行,设置下一次 alarm
if (taskData.repeat) {
await this.state.storage.setAlarm(Date.now() + taskData.intervalMs);
} else {
await this.state.storage.delete('taskData');
}
} catch (e) {
// alarm 失败时重试(指数退避)
const retryCount = (taskData.retryCount || 0) + 1;
if (retryCount <= 5) {
taskData.retryCount = retryCount;
await this.state.storage.put('taskData', taskData);
await this.state.storage.setAlarm(Date.now() + 1000 * Math.pow(2, retryCount));
}
}
}
}
3.4 常见坑点与避坑指南
坑点 1:并发请求的隐式排队
Durable Objects 是单线程的,但 Cloudflare 会将并发请求排队。如果你的 fetch handler 中有 await,后续请求会等待前一个请求完成。
// ❌ 错误:长时间操作阻塞后续请求
async fetch(request) {
const result = await this.longRunningOperation(); // 耗时 5 秒
return new Response(result);
}
// ✅ 正确:使用 waitUntil() 将长时间操作放到后台
async fetch(request) {
const promise = this.longRunningOperation();
this.state.waitUntil(promise); // 不阻塞响应
return new Response('Processing started');
}
坑点 2:存储 API 的配额限制
每个 Durable Object 的存储上限是 50 GB(可申请提升),但更关键的限制是:单个 put() 调用的值不能超过 128 KB,单个 get() 最多返回 1 MB。对于大对象,需要分片存储。
坑点 3:跨数据中心迁移
当一个 Durable Object 被频繁访问时,Cloudflare 会将它迁移到离请求源最近的数据中心。但这个迁移不是即时的——在迁移过程中,请求会被路由到旧位置。如果你的应用对延迟敏感,需要考虑这个行为。
💡 四、Durable Objects vs 替代方案
| 特性 | Durable Objects | Redis + Workers | Cloudflare KV | Cloudflare D1 |
|---|---|---|---|---|
| 一致性模型 | 强一致(单实例) | 强一致(同区域) | 最终一致 | 强一致 |
| 读延迟 | <1ms | 1-5ms | <1ms | 1-10ms |
| 写延迟 | <1ms | 1-5ms | <1ms(传播需 60s) | 5-50ms |
| WebSocket 支持 | ✅ 原生 | ❌ | ❌ | ❌ |
| 适用场景 | 有状态实时应用 | 缓存、计数器 | 配置、静态数据 | 关系型查询 |
| 全球分布 | ✅ 自动 | 需要集群 | ✅ | 单区域 |
⚡ 关键结论:如果你的应用需要强一致性 + 实时通信 + 边缘低延迟,Durable Objects 是目前唯一的选择。如果只需要简单的键值存储,用 KV 或 D1 更经济。
🎯 总结
Durable Objects 不是银弹,但它填补了边缘计算中有状态处理的空白。在以下场景中,它是最佳选择:
- ✅ 实时协作应用(文档编辑、白板、代码协作)
- ✅ 分布式限流和计数器
- ✅ 聊天室和在线状态管理
- ✅ 边缘任务调度和 Alarm
- ✅ 需要全局唯一性的分布式锁
避免在以下场景使用:
- ❌ 高吞吐写入(>1000 req/s 单实例瓶颈)
- ❌ 大量数据存储(50 GB 上限,且成本高于 R2)
- ❌ 纯读取场景(KV 更经济)
相关资源:
- 🔧 Cloudflare Durable Objects 官方文档
- 🔧 Wrangler CLI — Durable Objects 开发工具
- 🔧 Cloudflare Workers Playground — 在线测试
- 🔧 本文 JSON 处理工具 — jsjson.com 提供的在线 JSON 格式化和校验工具