Durable Objects
这一课你将能做到
- 创建一个 Durable Object 类并通过 Worker 路由访问
- 用 Durable Object 实现强一致的计数器、聊天室、协同编辑状态
- 理解 Durable Object 的「单线程」语义和存储模型
为什么要学
KV 最终一致、Queue 至少一次、D1 写走中心——这些都不适合「强一致、低延迟、高并发」的场景。比如:
- 实时聊天室:谁在线、消息顺序、未读数
- 协同编辑:多个用户同时改同一文档
- 游戏房间:玩家状态、回合制、实时同步
人话:Durable Object 就是一个「全局唯一的、有状态的、单线程的」对象实例。全世界只有一个 chatroom-123 实例,所有对 chatroom-123 的请求都会路由到同一个节点、同一个线程、同一份内存状态。这就是强一致的来源。
先建立图像
普通 Worker: Durable Object:
[请求1] ──> [北京节点] [请求1] ──> [chatroom-123 实例]
[请求2] ──> [纽约节点] [请求2] ──> [chatroom-123 实例]
[请求3] ──> [伦敦节点] [请求3] ──> [chatroom-123 实例]
│ │ │
▼ ▼ ▼
无共享状态 无共享状态 共享内存状态 + 持久化存储
跟着做
步骤 1:定义 Durable Object 类
export class ChatRoom {
state: DurableObjectState;
constructor(state: DurableObjectState) {
this.state = state;
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/join') {
const userId = url.searchParams.get('userId')!;
const users = await this.state.storage.get<string[]>('users') ?? [];
if (!users.includes(userId)) {
users.push(userId);
await this.state.storage.put('users', users);
}
return Response.json({ users });
}
if (url.pathname === '/message') {
const { userId, text } = await request.json();
const messages = await this.state.storage.get<any[]>('messages') ?? [];
messages.push({ userId, text, timestamp: Date.now() });
await this.state.storage.put('messages', messages);
return Response.json({ messages });
}
return new Response('Not Found', { status: 404 });
}
}
步骤 2:在 Worker 中路由到 Durable Object
wrangler.toml:
name = "chat-worker"
main = "src/index.ts"
[[durable_objects.bindings]]
name = "CHAT_ROOM"
class_name = "ChatRoom"
export interface Env {
CHAT_ROOM: DurableObjectNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const roomId = url.searchParams.get('roomId') ?? 'default';
// 根据 roomId 获取或创建 Durable Object 实例
const id = env.CHAT_ROOM.idFromName(roomId);
const stub = env.CHAT_ROOM.get(id);
// 把请求转发给 Durable Object
return stub.fetch(request);
},
} satisfies ExportedHandler<Env>;
步骤 3:用 WebSocket 实现实时聊天
export class ChatRoom {
state: DurableObjectState;
sessions: Map<WebSocket, string> = new Map();
constructor(state: DurableObjectState) {
this.state = state;
}
async fetch(request: Request): Promise<Response> {
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader !== 'websocket') {
return new Response('Expected WebSocket', { status: 426 });
}
const [client, server] = Object.values(new WebSocketPair());
await this.handleSession(server);
return new Response(null, { status: 101, webSocket: client });
}
async handleSession(ws: WebSocket) {
ws.accept();
this.sessions.set(ws, 'anonymous');
ws.addEventListener('message', async (event) => {
const data = JSON.parse(event.data as string);
if (data.type === 'join') {
this.sessions.set(ws, data.userId);
this.broadcast({ type: 'join', userId: data.userId });
}
if (data.type === 'message') {
this.broadcast({ type: 'message', userId: this.sessions.get(ws), text: data.text });
}
});
ws.addEventListener('close', () => {
const userId = this.sessions.get(ws);
this.sessions.delete(ws);
this.broadcast({ type: 'leave', userId });
});
}
broadcast(message: any) {
const payload = JSON.stringify(message);
for (const [ws] of this.sessions) {
ws.send(payload);
}
}
}
步骤 4:持久化存储
Durable Object 有内置的 storage API,数据会持久化到磁盘:
// 写入
await this.state.storage.put('key', value);
// 读取
const value = await this.state.storage.get('key');
// 事务
await this.state.storage.transaction(async (txn) => {
const count = await txn.get<number>('count') ?? 0;
await txn.put('count', count + 1);
});
讲清楚要点
- 单线程语义:每个 Durable Object 实例在同一时间只处理一个请求。这意味着你不需要锁,状态天然线程安全。但也意味着慢操作会阻塞后续请求。
- 全局唯一:
idFromName("room-123") 在全球任何地方都会得到同一个实例 ID,请求会被路由到该实例所在的节点。
- 内存 + 持久化:Durable Object 有内存(
this.sessions)和持久化存储(this.state.storage)。内存快但重启丢失,持久化慢但可靠。
- WebSocket 原生支持:Durable Object 是 Workers 中唯一支持 WebSocket 长连接的方式。配合
hibernation 可以实现低成本长连接。
- 计费模式特殊:按「活跃时长」计费。一个 DO 实例只要还在处理请求或持有 WebSocket 连接,就在计费。空闲时会自动休眠。
你可能会懵的地方
-
Durable Object 和普通 Worker 怎么选?
需要强一致、共享状态、WebSocket 用 DO;无状态、纯计算、转发用普通 Worker。DO 成本更高,不要滥用。
-
DO 的存储有大小限制吗?
单实例存储上限约 1GB(可申请更高)。超过的话考虑分片(多个 DO 实例)或配合 R2。
-
DO 实例会迁移吗?
会。Cloudflare 会根据负载和地理位置自动迁移 DO 实例。迁移过程中会有短暂延迟(毫秒级),但状态不会丢。
-
DO 支持定时任务吗?
支持 state.storage.setAlarm(),可以设置未来的某个时间点触发回调。适合做房间超时清理、游戏回合倒计时。
-
DO 能访问 KV、D1、R2 吗?
可以。DO 的 fetch 方法接收的 env 和普通 Worker 一样,可以绑定各种资源。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 在 DO 里做 CPU 密集计算 | 单线程会阻塞所有请求 | 把重计算丢给 Queue 或外部服务 |
不调用 ws.accept() | WebSocket 连接无法建立 | handleSession 第一件事就是 ws.accept() |
| 用 DO 存大量历史消息 | 单实例存储有限,查询慢 | 历史消息放 D1 或 R2,DO 只存最近状态 |
| 期望 DO 自动扩容 | DO 是单实例,不自动横向扩容 | 分片:按 roomId / userId 创建多个 DO 实例 |
| 在 DO 构造函数里做异步操作 | 构造函数不能是 async | 用 state.blockConcurrencyWhile() 做初始化 |
卡点急救
idFromName 返回的 ID 无效:确保 class_name 和 wrangler.toml 里的 [[durable_objects.bindings]] 一致;确保类已 export。
- WebSocket 连接立刻断开:检查是否调用了
ws.accept();检查 DO 实例是否因为空闲被休眠(用 hibernation API 或定期 ping)。
- DO 存储读写慢:持久化存储是磁盘 I/O,比内存慢。高频读写的数据放内存,定期 flush 到 storage。
面试常问
-
Durable Object 的一致性模型是什么?
强一致。单实例单线程,所有请求串行处理,内存状态实时可见。持久化存储通过事务保证原子性。
-
DO 怎么实现分布式锁?
利用单线程语义:所有请求串行执行,天然互斥。不需要额外锁机制。也可以用 storage.transaction() 实现原子操作。
-
DO 和 Redis 怎么选?
DO 适合「强一致、低延迟、有状态」的场景(聊天室、游戏、协同编辑);Redis 适合「缓存、简单计数、Pub/Sub」的场景。DO 和 Worker 同节点运行,延迟更低;Redis 需要网络往返。
自测三问
- Durable Object 为什么能保证强一致?
- 如果聊天室有 10 万用户,你会怎么设计 DO 的架构?
- DO 的
state.storage 和 this.xxx 内存变量有什么区别?
小结一句话
Durable Objects 是 Workers 生态中的「强一致状态原语」:全局唯一、单线程、内存 + 持久化,适合聊天室、协同编辑、游戏房间等需要共享状态的场景。
延伸