Durable Object 的四个关键词
Unique Instance
每个 DO id 对应全球唯一一个实例。10 万个聊天室 = 10 万个 DO,不重合。
Strong Consistency
一个 DO 内的 storage 写入立刻对自己的后续读可见,没有最终一致问题。
Single-threaded
一个 DO 同时只处理一个请求——天然互斥,不需要锁。也意味着 DO 是单点瓶颈,不适合极高并发的单对象。
Co-located Storage
每个 DO 有自带的存储(2025 后默认 SQLite,可存几 GB 且支持完整 SQL)。读写是本地 IO,零网络延迟。
最小示例:计数器
// src/counter.ts import { DurableObject } from 'cloudflare:workers'; export class Counter extends DurableObject { async increment(): Promise<number> { const cur = (await this.ctx.storage.get<number>('n')) ?? 0; const next = cur + 1; await this.ctx.storage.put('n', next); return next; } }
// src/index.ts app.post('/counter/:name', async (c) => { const id = c.env.COUNTER.idFromName(c.req.param('name')); const stub = c.env.COUNTER.get(id); const n = await stub.increment(); return c.json({ count: n }); });
# wrangler.toml [[durable_objects.bindings]] name = "COUNTER" class_name = "Counter" [[migrations]] tag = "v1" new_sqlite_classes = ["Counter"] # 2025 新 DO 全部用 SQLite storage
重点:idFromName("room-123") 把字符串 hash 到一个 DO 上——同名字符串永远到同一个 DO。这就是"按 key 分片到全球唯一实例"的入口。
SQLite Storage:DO 的新武器
2025 年起,新建 DO 默认带 SQLite。每个 DO 有最多 10GB 空间,支持完整 SQL:
export class ChatRoom extends DurableObject { constructor(ctx, env) { super(ctx, env); ctx.storage.sql.exec(` CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT, body TEXT, ts INTEGER ) `); } async post(user: string, body: string) { this.ctx.storage.sql.exec( 'INSERT INTO messages (user, body, ts) VALUES (?, ?, ?)', user, body, Date.now(), ); } async recent(limit = 50) { return this.ctx.storage.sql .exec('SELECT * FROM messages ORDER BY id DESC LIMIT ?', limit) .toArray(); } }
WebSocket Hibernation
DO 的杀手应用:用 WebSocket 做协同。普通 WebSocket 长连接意味着 DO 永远在内存——账单爆炸。Hibernation 解决这个:连接保持,DO 睡眠不收费,只在真正收到消息时唤醒。
export class ChatRoom extends DurableObject { async fetch(req: Request) { if (req.headers.get('upgrade') !== 'websocket') { return new Response('expects ws', { status: 426 }); } const pair = new WebSocketPair(); const [client, server] = Object.values(pair); this.ctx.acceptWebSocket(server); // 启用 hibernation return new Response(null, { status: 101, webSocket: client }); } async webSocketMessage(ws: WebSocket, msg: string) { // DO 只在这里被唤醒 const peers = this.ctx.getWebSockets(); for (const p of peers) if (p !== ws) p.send(msg); } async webSocketClose(ws: WebSocket, code: number) { ws.close(code); } }
500 个观众的直播房间,99% 时间没人说话——DO 都在睡眠,只有消息/上下线才唤醒,成本是连接不收费 + 唤醒一次几厘钱。
速率限制
按 user_id 路由到 DO,DO 内部记滑动窗口:
export class RateLimiter extends DurableObject { async check(window: number, max: number): Promise<boolean> { const now = Date.now(); const cutoff = now - window; this.ctx.storage.sql.exec( `CREATE TABLE IF NOT EXISTS hits (ts INTEGER)`, ); this.ctx.storage.sql.exec('DELETE FROM hits WHERE ts < ?', cutoff); const [{ c }] = this.ctx.storage.sql .exec('SELECT COUNT(*) AS c FROM hits').toArray(); if (c >= max) return false; this.ctx.storage.sql.exec('INSERT INTO hits (ts) VALUES (?)', now); return true; } }
Alarm:延时 / 定时
DO 可以给自己设 alarm:
await this.ctx.storage.setAlarm(Date.now() + 60_000); async alarm() { // 一分钟后被 Cloudflare 唤醒执行这里 console.log('timeout fired'); }
典型用法:会话超时、未支付订单关闭、订阅续费检查。
路由策略:按什么 key 分 DO
按 room / doc / channel
协同编辑:一个文档一个 DO,所有该文档的操作都在这里序列化。
按 user
速率限制、会话状态:每个用户一个 DO。
按资源
库存、座位图:一个座位一个 DO,扣减强一致。
不要把所有东西路到一个 DO
idFromName("global") 全局单例会立刻成为瓶颈——DO 是单线程的。分片永远把 key 设计成有独立热点的维度(用户、文档、对象)。
容量与成本
| 维度 | 数字 |
|---|---|
| 单 DO SQLite 上限 | 10GB |
| DO 数量 | 实际无上限(按 class 计费) |
| 请求成本 | $0.15 / 百万请求 |
| SQLite 读行 | $0.001 / 百万 |
| 活跃持续时长 | $12.50 / 百万 GB-秒 |
本章小结
- DO = 全球唯一 Actor,强一致,单线程,自带 SQLite 存储
- idFromName() 把 key 映射到唯一 DO,像分片键
- WebSocket Hibernation 让长连接不计费,适合聊天/直播
- Alarm 做延时任务,无需外部调度器
- 禁忌:不要让所有流量路到一个 DO(单线程瓶颈)