Cloudflare Workers 默认是无状态的 V8 isolate,但业务几乎都需要存储状态。Cloudflare 提供了两种核心存储原语:**KV(全局分布式键值存储)**和 Durable Objects(有状态边缘对象)。两者在一致性模型、延迟特性和适用场景上截然不同。本文从原理到生产代码,完整覆盖两种存储的选择与实战。
一、KV 存储:最终的全球性
1.1 定位与核心特性
KV 是 Cloudflare 的全球分布式键值存储,设计目标:让配置、元数据、缓存内容在全球任意边缘节点都能以极低延迟读取。
| 特性 | 说明 |
|---|---|
| 一致性模型 | 最终一致性(写入后 1 分钟内全局传播) |
| 读取延迟 | < 10ms(边缘节点内存缓存) |
| 写入延迟 | < 1s(异步复制到全球数据中心) |
| 键大小限制 | 512KB 键名,25MB 值 |
| 存储限额 | 免费 1GB,付费无上限 |
| TTL 支持 | ✅ 键级别过期时间 |
| 列表扫描 | 支持前缀匹配,性能随前缀减少提升 |
1.2 写入流程与一致性
Worker (东京) → 写入 KV (key="user:123")
↓
KV 写入到最近的中央存储节点(Core DC)
↓
异步复制到全球边缘节点(Edge POP)
↓
Worker (纽约) → 读取 KV (key="user:123")
→ 可能读到旧值(复制延迟 < 60s)
关键认知:KV 不是实时同步的。如果你写入后立刻在另一个 Worker 中读取,可能拿到旧数据。不要用它做实时计数器、锁、或要求强一致性的业务。
1.3 绑定与使用
// wrangler.toml
[[kv_namespaces]]
binding = "MY_KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
// src/index.ts
export interface Env {
MY_KV: KVNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// 写入
await env.MY_KV.put('user:123', JSON.stringify({ name: 'Alice', age: 30 }));
await env.MY_KV.put('session:abc', 'data', { expirationTtl: 3600 }); // 1 小时后过期
// 读取
const user = await env.MY_KV.get('user:123');
const userWithMetadata = await env.MY_KV.getWithMetadata('user:123');
// userWithMetadata.value = JSON string
// userWithMetadata.metadata = { ...custom metadata... }
// 删除
await env.MY_KV.delete('user:123');
// 列表(前缀匹配)
const list = await env.MY_KV.list({ prefix: 'user:' });
// list.keys = [{ name: 'user:123', expiration: ... }, ...]
return Response.json({ user: user ? JSON.parse(user) : null });
},
};
1.4 缓存策略
KV 会自动在边缘节点缓存热点键值,缓存行为:
| 操作 | 缓存行为 |
|---|---|
get() | 边缘节点缓存,TTL 由 Cloudflare 内部决定(通常 60s) |
getWithMetadata() | 同 get() |
put() | 写入 Core,使旧缓存失效 |
delete() | 写入 Core,使旧缓存失效 |
list() | 结果不缓存 |
强制刷新缓存:连续两次相同 key 的 put() 会确保旧缓存被清除,但新值传播仍需要时间。
1.5 KV 最佳场景
| 场景 | 原因 |
|---|---|
| 配置存储 | 配置变更不频繁,最终一致可接受 |
| A/B 测试标志 | 可以容忍几秒的传播延迟 |
| 博客文章元数据 | 写少读多,变更后可等几秒 |
| 翻译字典 | 几乎只读,完美适合 |
| 静态资源缓存 | HTML/JSON 的 KV 缓存层 |
| GeoIP/黑名单 | 更新后缓慢传播可接受 |
不适合的场景:实时计数器、库存扣减、会话状态、分布式锁、聊天消息。
二、Durable Objects:有状态的保证
2.1 定位与核心特性
Durable Objects(DO)是 Cloudflare 提供的有状态计算单元:每个 DO 是一个独立的 JavaScript 对象,绑定到一个唯一的 ID,保证单线程执行和 ACID 事务一致性。
| 特性 | 说明 |
|---|---|
| 一致性 | 强一致性(单线程,无并发写入冲突) |
| 状态持久化 | 内存 + 磁盘双保险,Worker 重启后状态恢复 |
| 位置 | 首次访问时"创建"在最近的 Core DC |
| WebSocket | ✅ 原生支持,可维持长连接 |
| 事务 | 内存内状态变更天然原子 |
| 计费 | 请求数 + 内存使用时长(GB-秒) |
2.2 DO 的生命周期
首次访问 DO (id="room:abc")
↓
Cloudflare 路由到最近的 Core DC
↓
创建 JavaScript 对象实例(调用 constructor)
↓
状态从持久化存储加载到内存
↓
处理请求(fetch / WebSocket / alarm)
↓
一段时间无请求 → 内存释放(hibernation),但状态保留
↓
再次访问 → 从持久化恢复状态,重新创建实例
2.3 两种 ID 生成方式
// 方式一:Name-based ID(推荐)
// 基于字符串生成确定性 ID,相同 name 始终路由到同一个 DO
const id = env.MY_DO.idFromName('room:game-123');
const doStub = env.MY_DO.get(id);
// 方式二:Unique ID(UUID)
// 生成全新的唯一 ID,用于创建全新对象
const id = env.MY_DO.newUniqueId();
const doStub = env.MY_DO.get(id);
2.4 基础 CRUD 示例
// src/index.ts
export interface Env {
MY_DO: DurableObjectNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const roomId = url.pathname.split('/')[2]; // /room/:id
// 获取或创建 DO
const id = env.MY_DO.idFromName(`room:${roomId}`);
const room = env.MY_DO.get(id);
// 转发请求到 DO
return room.fetch(request);
},
};
// src/Room.ts
export class ChatRoom implements DurableObject {
private state: DurableObjectState;
private sessions: Map<WebSocket, string> = new Map(); // 连接的 WebSocket
constructor(state: DurableObjectState) {
this.state = state;
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
switch (url.pathname) {
case '/connect': {
// WebSocket 升级
const [client, server] = Object.values(new WebSocketPair());
this.handleSession(server);
return new Response(null, { status: 101, webSocket: client });
}
case '/message': {
// HTTP 发送消息(广播给所有连接的 WebSocket)
const { message } = await request.json();
this.broadcast(message);
return Response.json({ sent: true });
}
case '/state': {
// 读取持久化状态
const stored = await this.state.storage.get('messages') || [];
return Response.json({ messages: stored });
}
default:
return new Response('Not Found', { status: 404 });
}
}
private handleSession(ws: WebSocket) {
this.state.acceptWebSocket(ws); // 接受 WebSocket
this.sessions.set(ws, 'anonymous');
// 发送历史消息
this.state.storage.get('messages').then((history: any) => {
if (history) ws.send(JSON.stringify({ type: 'history', data: history }));
});
}
private async broadcast(message: string) {
// 保存到持久化存储
const history = (await this.state.storage.get('messages')) || [];
history.push({ message, time: Date.now() });
await this.state.storage.put('messages', history);
// 广播给所有 WebSocket
for (const [ws] of this.sessions) {
ws.send(JSON.stringify({ type: 'message', data: message }));
}
}
}
# wrangler.toml
[[durable_objects.bindings]]
name = "MY_DO"
class_name = "ChatRoom"
script_name = "my-worker"
2.5 DO Alarms(定时任务)
export class Counter implements DurableObject {
private state: DurableObjectState;
constructor(state: DurableObjectState) {
this.state = state;
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/schedule') {
// 设置 30 秒后触发的 alarm
await this.state.storage.setAlarm(Date.now() + 30000);
return Response.json({ scheduled: true });
}
return Response.json({ count: await this.state.storage.get('count') || 0 });
}
async alarm() {
// 定时执行的任务
const count = (await this.state.storage.get('count') || 0) as number;
await this.state.storage.put('count', count + 1);
console.log(`Counter incremented to ${count + 1}`);
}
}
三、KV vs Durable Objects:选择决策矩阵
| 维度 | KV | Durable Objects |
|---|---|---|
| 一致性 | 最终一致 | 强一致 |
| 读取延迟 | < 10ms | 20-100ms(取决于 DO 所在位置) |
| 写入延迟 | < 1s | < 100ms(同 Core DC) |
| 并发安全 | ❌ 可能冲突 | ✅ 单线程保证 |
| 事务 | ❌ | ✅ 原子操作 |
| WebSocket | ❌ | ✅ 原生支持 |
| 存储大小 | 25MB/值 | 有限(内存+磁盘,建议 < 1GB) |
| 计费 | 读/写请求数 | 请求 + 内存 × 时长 |
| 适用场景 | 配置、缓存、元数据 | 会话、计数器、协作、游戏、锁 |
决策树
需要强一致性或事务?
├── 是 → 需要 WebSocket/长连接?
│ ├── 是 → Durable Objects ✅
│ └── 否 → 读多写少且数据小?
│ ├── 是 → Durable Objects ✅
│ └── 否 → 考虑 D1 数据库
└── 否 → 写后立刻从其他地点读取?
├── 是 → KV 不满足(可能有旧值)→ Durable Objects
└── 否 → KV ✅(最终一致可接受)
四、生产实战场景
4.1 全局计数器(DO)
export class Counter implements DurableObject {
constructor(private state: DurableObjectState) {}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/increment') {
let count = (await this.state.storage.get('count')) || 0;
count++;
await this.state.storage.put('count', count);
return Response.json({ count });
}
return Response.json({ count: await this.state.storage.get('count') || 0 });
}
}
// 外部调用
const id = env.COUNTER.idFromName('global-counter');
const counter = env.COUNTER.get(id);
const res = await counter.fetch('https://fake-url/increment');
4.2 用户会话(KV)
// 登录时创建会话
await env.SESSIONS.put(
`session:${sessionId}`,
JSON.stringify({ userId, role, loginAt: Date.now() }),
{ expirationTtl: 86400 } // 24 小时过期
);
// 验证会话
const session = await env.SESSIONS.get(`session:${sessionId}`);
if (!session) return new Response('Unauthorized', { status: 401 });
const { userId, role } = JSON.parse(session);
4.3 分布式速率限制(DO)
export class RateLimiter implements DurableObject {
constructor(private state: DurableObjectState) {}
async fetch(request: Request): Promise<Response> {
const ip = request.headers.get('CF-Connecting-IP');
const key = `rate:${ip}`;
const window = 60 * 1000; // 1 分钟窗口
const limit = 100; // 100 请求/分钟
const now = Date.now();
const data = (await this.state.storage.get(key)) || { count: 0, resetAt: now + window };
if (now > data.resetAt) {
data.count = 0;
data.resetAt = now + window;
}
data.count++;
await this.state.storage.put(key, data);
if (data.count > limit) {
return new Response('Rate Limited', { status: 429 });
}
return Response.json({ remaining: limit - data.count });
}
}
4.4 协作白板(DO + WebSocket)
export class Whiteboard implements DurableObject {
private webSockets = new Set<WebSocket>();
constructor(private state: DurableObjectState) {}
async fetch(request: Request): Promise<Response> {
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader !== 'websocket') {
return new Response('Expected websocket', { status: 400 });
}
const [client, server] = Object.values(new WebSocketPair());
this.state.acceptWebSocket(server);
this.webSockets.add(server);
server.addEventListener('message', async (msg) => {
const data = JSON.parse(msg.data as string);
// 保存历史
const history = (await this.state.storage.get('strokes')) || [];
history.push(data);
await this.state.storage.put('strokes', history);
// 广播给其他客户端
for (const ws of this.webSockets) {
if (ws !== server && ws.readyState === WebSocket.READY_STATE_OPEN) {
ws.send(JSON.stringify(data));
}
}
});
return new Response(null, { status: 101, webSocket: client });
}
}
五、性能基准
5.1 KV 读取延迟
| 操作 | 延迟(全球平均) |
|---|---|
KV get(缓存命中) | 1-5ms |
KV get(首次) | 5-15ms |
KV put | 200-500ms |
KV put + 传播 | 1-60s(最终一致) |
5.2 DO 操作延迟
| 操作 | 延迟 |
|---|---|
DO fetch(已 warm) | 20-50ms |
DO fetch(cold start) | 100-300ms |
| 状态读写(storage) | 5-20ms |
| WebSocket 消息转发 | < 10ms |
常见问题(FAQ)
KV 写入后多久能全球读取到新值?
通常 1-60 秒。不要依赖 KV 的即时一致性。如果需要强一致,用 Durable Objects。
Durable Objects 能存储多少数据?
建议每个 DO 存储 < 1GB。DO 的状态持久化到磁盘,但内存中保持活跃对象。过大的 DO 会影响恢复速度。
DO 的 WebSocket 连接数有限制吗?
每个 DO 建议维持 < 1000 个并发 WebSocket 连接。如果需要更多,考虑分片(用多个 DO)。
KV 和 DO 能一起用吗?
可以,而且是推荐模式:
- KV 存储全局配置、A/B 标志、静态内容
- DO 存储需要强一致的状态、会话、协作数据
- 在 DO 中读取 KV 配置来驱动业务逻辑
DO 的计费贵吗?
| 维度 | 费用 |
|---|---|
| 请求 | $0.12/百万次 |
| 内存(GB-小时) | $12.50/GB-月 |
| WebSocket 消息 | $0.25/百万条 |
一个小型聊天室 DO(128MB 内存,24h 运行)约 $0.40/天。大多数应用 DO 成本 <$10/月。
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。