blog.dopana

Back

Durable Objects(DO)是 Cloudflare Workers 的有状态协同解决方案。每个 DO 拥有全球唯一的运行实例 —— 状态持久存在,并具备强一致性保证。

flowchart LR
    subgraph EdgeWorkers["无状态 Workers(全球边缘节点)"]
        direction TB
        W_Tokyo["Worker 东京"]
        W_London["Worker 伦敦"]
        W_US["Worker 美国"]
    end

    subgraph DO_Instance["Durable Object(全球唯一实例)"]
        direction TB
        ID["ID: 'user-room-42'"]
        
        subgraph Memory["RAM(内存状态)"]
            State["活跃连接与状态数据"]
        end
        
        subgraph Storage["持久化存储"]
            SQL[("SQLite / KV Storage")]
        end

        ID --> Memory
        Memory <--> Storage
    end

    W_Tokyo -->|&quot;所有相同 ID 的请求<br/>路由至单一实例&quot;| DO_Instance
    W_London -->|&quot;强一致性<br/>(杜绝竞态条件)&quot;| DO_Instance
    W_US -->|&quot;状态持续驻留&quot;| DO_Instance

Workers vs Durable Objects#

flowchart TB
    subgraph Clients["客户端与浏览器"]
        C1["用户 A (WebSocket)"]
        C2["用户 B (WebSocket)"]
        C3["用户 C (HTTP REST)"]
    end

    subgraph Edge["Cloudflare 全球边缘(无状态 Workers)"]
        W1["Worker(边缘节点 1)"]
        W2["Worker(边缘节点 2)"]
    end

    subgraph DO_Cluster["Durable Objects(每个 ID 对应单一协调实例)"]
        subgraph DO1["Chat Room DO (ID: room-123)"]
            Mem["内存状态 & 活跃 WebSocket"]
            SQL[(&quot;内置 SQLite / KV 存储&quot;)]
            Alarm["定时器 / Alarms"]
            Mem <--> SQL
            Mem <--> Alarm
        end
        subgraph DO2["Chat Room DO (ID: room-456)"]
            Mem2["内存状态"]
            SQL2[(&quot;SQLite 存储&quot;)]
        end
    end

    C1 <-->|&quot;边缘连接&quot;| W1
    C2 <-->|&quot;边缘连接&quot;| W2
    C3 -->|&quot;HTTP 请求&quot;| W1

    W1 <-->|&quot;路由至 ID: room-123&quot;| DO1
    W2 <-->|&quot;路由至 ID: room-123&quot;| DO1
    W1 -.->|&quot;路由至 ID: room-456&quot;| DO2
WorkersDurable Objects
无状态(Stateless)有状态(Stateful)
多个请求产生多个实例每个 ID 对应全球唯一实例
内存不保留状态在内存 + 存储中持久保留状态
无限自动水平扩展通过分片(Sharding 多 DO)扩展
分布在 330+ 个数据中心在单一位置执行(支持热迁移)
------

聊天室 — 基础示例#

sequenceDiagram
    autonumber
    actor ClientA as 用户 Alice
    actor ClientB as 用户 Bob
    participant Worker as 无状态 Worker 路由
    participant DO as ChatRoom [Durable Object]

    Note over ClientA, DO: 初始化 WebSocket 连接
    ClientA->>Worker: GET /?room=general&name=Alice (Upgrade: WebSocket)
    Worker->>DO: stub.fetch(request) [idFromName("general")]
    DO->>DO: server.accept(), 保存会话 "Alice"
    DO-->>ClientA: 101 Switching Protocols (WebSocket 已连接)
    DO--)ClientB: 广播 "Alice joined the chat"

    Note over ClientA, DO: 实时消息广播
    ClientA->>DO: WS 消息: "Hello room!"
    DO->>DO: broadcast("Alice: Hello room!", sender=Alice)
    DO--)ClientB: WS 消息: "Alice: Hello room!"

SQLite Storage — 内置存储引擎#

每个 Durable Object 均拥有独立的 SQLite 存储:

flowchart TD
    Req["请求: GET /increment /get /reset"] --> Match{url.pathname}
    Match -->|&quot;/increment&quot;| Inc["storage.get('count')<br/>storage.put('count', count + 1)"]
    Match -->|&quot;/get&quot;| Get["storage.get('count')"]
    Match -->|&quot;/reset&quot;| Res["storage.put('count', 0)"]
    Match -->|其他| Err["404 Not Found"]
    
    Inc --> Resp["返回 JSON count"]
    Get --> Resp
    Res --> Resp

使用 storage.sql 执行 SQL 查询#

直接在 Durable Objects 中执行原生 SQL:

flowchart TD
    Req["HTTP 请求"] --> Init["表不存在则创建: CREATE TABLE IF NOT EXISTS todos (...)"]
    Init --> Router{HTTP 方法}
    
    Router -->|&quot;GET&quot;| Q1["SELECT * FROM todos ORDER BY id DESC"]
    Router -->|&quot;POST&quot;| Q2["INSERT INTO todos (title) VALUES (?)"]
    Router -->|&quot;PUT&quot;| Q3["UPDATE todos SET completed = ? WHERE id = ?"]
    Router -->|&quot;DELETE&quot;| Q4["DELETE FROM todos WHERE id = ?"]
    Router -->|其他| Q5["405 Method Not Allowed"]

    Q1 --> Out1["JSON: todos 列表"]
    Q2 --> Out2["201 Created: 新创建的 todo"]
    Q3 --> Out3["JSON: &#123; success: true &#125;"]
    Q4 --> Out4["JSON: &#123; success: true &#125;"]

Alarms — DO 定时任务与调度#

sequenceDiagram
    autonumber
    actor User as 用户 / 客户端
    participant DO as Reminder DO
    participant Storage as ctx.storage [Alarm 队列]
    participant Webhook as 外部 Webhook

    Note over DO, Storage: 1. 注册定时器 / 闹钟
    User->>DO: GET /?delay=5000
    DO->>Storage: setAlarm(futureTimestamp)
    DO-->>User: {"message": "Alarm set for 5000ms"}

    Note over Storage, Webhook: 2. 超时唤醒执行(唤醒 DO)
    Storage-->>DO: 触发 alarm() 处理器
    DO->>Webhook: POST https://hooks.example.com/notify { event: 'alarm_fired' }
    DO->>Storage: setAlarm(nextHourTimestamp) [设置下次循环定时]

多人在线游戏服务器#

flowchart TD
    WS["WebSocket 消息事件"] --> EventType{msg.type}

    EventType -->|&quot;join&quot;| Join["1. 将玩家会话加入 Map<br/>2. 初始化状态: x=0, y=0<br/>3. 广播: type: 'players'"]
    EventType -->|&quot;move&quot;| Move["1. 更新坐标: player.x += dx, player.y += dy<br/>2. 广播: type: 'move'"]
    EventType -->|&quot;shoot&quot;| Shoot["1. 计算子弹方向与角度<br/>2. 广播: type: 'shoot'"]

    Close["WebSocket 断开事件"] --> Leave["1. 从 Map 和状态中移除玩家<br/>2. 广播: type: 'leave'"]

    Join --> BroadcastAll["向房间内所有连接玩家广播更新后的状态"]
    Move --> BroadcastAll
    Shoot --> BroadcastAll
    Leave --> BroadcastAll

迁移管理 — 跨版本迁移 DO#

Durable Objects 支持零停机在不同类和架构间进行迁移:

// wrangler.jsonc
{
  "durable_objects": {
    "bindings": [
      {
        "name": "COUNTER",
        "class_name": "Counter",
        "migration": "new_tag"
      }
    ]
  }
}
typescript

迁移类型:

  • new_tag — 新类标签
  • new_classes — 添加多个类
  • rename — 类重命名
  • transfer — 命名空间之间的数据迁移

设计模式 — 分片(Sharding)#

每个 Durable Object 实例在同一时刻仅运行于单个物理位置。为了实现横向扩展,通常通过 Key 进行分片:

flowchart LR
    Req["请求: userId"] --> Formula["shardId = floor(userId / 1000)"]
    Formula --> ShardMap{分片路由}
    
    ShardMap -->|&quot;userId: 0 - 999&quot;| S0["DO 实例: shard-0"]
    ShardMap -->|&quot;userId: 1000 - 1999&quot;| S1["DO 实例: shard-1"]
    ShardMap -->|&quot;userId: 2000 - 2999&quot;| S2["DO 实例: shard-2"]
    ShardMap -->|&quot;userId: N - N+999&quot;| Sn["DO 实例: shard-N"]

总结#

Durable Objects 完美解决了无服务器架构中的状态管理难题:

  • 实时通信:WebSocket 连接结合全球强一致性内存状态
  • 游戏服务器:低延迟多人在线状态协同
  • 分布式协同:全局分布式锁、选主、速率限制
  • 内置 SQLite:无需维护外部数据库即可进行全功能关系型查询

架构设计建议:无状态请求由 Workers 处理,需要一致性状态协同的场景交由 Durable Objects。通过 Key 进行分片以实现无限水平扩展。

参考文献#