KernelRanch
返回文章列表

Durable Objects 入门:一个 ID 一个实例的强一致状态

2026-11-10 #Cloudflare

读完这一篇,你能说清 Durable Objects 解决的是哪一类问题,能照着写出一个带持久状态的计数器类,知道 migrations 那一段配置为什么不能省,也知道什么时候该用它、什么时候用 KV 或 D1 更省事。

需要提前说明:这个账号的 token 没有 Durable Objects 的写权限,本篇没有部署,也没有在本机跑起来验证。配置和代码按官方文档的写法给出,实际输出无法提供,第 6 节列了几条部署后应当核对的现象,供你自测时对照。

1. 它解决的是"同一个 key 的并发写"

KV 最终一致,D1 是共享的 SQLite,两者都不适合"同一个对象被高频读写并且要求立刻一致"的场景:抢票库存、房间内消息序号、每用户限流计数、协同编辑的文档状态。

DO 的模型是:给一个名字(或 ID)就能拿到全世界唯一的一个实例,这个实例单线程执行,请求在它内部排队。因为只有一个执行者,读改写不需要锁,也不会两个请求同时把库存从 1 减到 0 再减到 -1。状态存在实例自己的 storage 里,落盘后重启还在。

代价是吞吐上限:单个实例的请求是串行的,写热点集中在一个 ID 上时,后面的请求要排队。设计上把状态切细(每个用户一个 ID、每个房间一个 ID)就是为了分散这个队列。

2. 配置:类 + migrations

name = "cf62-do-demo"
main = "src/index.js"
compatibility_date = "2026-10-08"

[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["Counter"]

三处都要对:class_name 要和代码里导出的类同名,name 是 env 上的绑定名,migrations 里声明这个类是新建的。免费档要求 DO 使用 SQLite 存储后端,所以写 new_sqlite_classes 而不是老的 new_classes;用错会在部署时被拒绝。

tag 是迁移的版本标记,第一次用 v1,之后每次改动类的存储结构就加一条新 tag。已经上线的类不能删掉迁移记录,否则历史实例找不到归属。

3. 一个计数器

export class Counter {
  constructor(state, env) {
    this.state = state;
    this.env = env;
    // 初始化只跑一次,适合在这里读一次持久状态
    this.state.blockConcurrencyWhile(async () => {
      this.value = (await this.state.storage.get("value")) || 0;
    });
  }

  async fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === "/inc") {
      this.value += 1;
      await this.state.storage.put("value", this.value);
      return Response.json({ value: this.value });
    }

    if (url.pathname === "/get") {
      return Response.json({ value: this.value });
    }

    return new Response("Not Found", { status: 404 });
  }
}

state.storage 是 DO 专属的持久存储,get / put / delete / list 都是异步的,单次操作在实例内是原子的。内存里的 this.value 只是缓存,落盘靠 put;不 put 就重启丢失。

blockConcurrencyWhile 保证初始化完成前不处理任何请求。DO 的构造函数不能是 async,所以异步初始化只能放这里,直接 await 在构造函数里是语法错误。

4. 从 Worker 拿到实例

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const name = url.searchParams.get("name") || "global";
    const id = env.COUNTER.idFromName(name);
    const stub = env.COUNTER.get(id);
    return stub.fetch(request);
  }
};

idFromName("global") 把任意字符串映射成一个稳定 ID,同样的名字永远指向同一个实例。也可以 newUniqueId() 生成随机 ID,或者从外部传入的 ID 字符串 idFromString() 还原。

stub 是一个看起来像 Worker 的东西,stub.fetch(request) 会把请求转给实例。这意味着 DO 和 Worker 之间就是普通的 Request/Response,写法与 57-workers-request-response.md 里讲的一致。

新版运行时还支持 RPC 风格:类上直接写方法,stub.increment() 这样调用,省掉手写路径判断。两种风格可以共存,团队里统一一种更好维护。

5. 和 KV、D1 怎么分工

需求选谁
配置、白名单、读多写少KV,见 59-workers-kv-tutorial.md
关系表、按条件查询、报表D1,见 61-workers-d1-database.md
单对象高频读写的强一致状态DO
定时清理、延时任务DO 的 alarm,或 63-workers-cron-triggers.md

一条常见的组合是:业务数据放 D1,热点计数放 DO,配置放 KV。DO 里也能开 state.storage.sql 执行 SQL,适合"每个实例一个独立小库"的形态,比如每个房间的聊天记录。

6. 上线后该核对的现象

部署完成后,按这几条自测,任何一条不符都说明配置有问题:

  • wrangler deploy 的输出里,绑定区应出现 env.COUNTER 且类型是 Durable Object Namespace。
  • 连续请求 /inc 十次,返回的 value 应是 1 到 10 递增,不跳号不重号。
  • 用不同的 ?name= 请求,计数互相独立——这是"一个 ID 一个实例"最直观的验证。
  • 停一段时间再请求 /get,值应该还在,说明 put 真的落盘了。
  • 两个客户端同时打同一个 name 的 /inc,总次数应等于请求数,不会丢。

7. 常见问题

问:DO 的实例在哪个机房? 答:由第一个访问它的请求决定,之后固定在那个位置。所以跨洲访问同一个热点实例会有额外延迟,这也是要把状态按用户切分的原因之一。需要主动控制位置时可以用 location hints。

问:state.storage.put 每次都写盘,会不会很慢? 答:单次写有延迟,但 DO 会把短时间内的多次写合并到一次落盘。真正拖慢的是在 fetch 里做大量串行 await,能批量就 put 一个对象而不是十个 key。

问:免费档能用 DO 吗? 答:能,但有额度上限,并且要求 SQLite 后端,配置里要用 new_sqlite_classes。额度数字以控制台和 56-free-tier-limits-cheatsheet.md 为准。

小结

Durable Objects 的价值只有一个词:单实例。同一个 ID 全世界的请求都落到同一个单线程对象上,于是"读改写"不需要锁,状态存在对象自己身上,重启不丢。

用的门槛在配置:绑定要对、class_name 要对、migrations 里的 new_sqlite_classes 要对,三处任一处写错都部署不上去。代码本身很简单——构造函数里 blockConcurrencyWhile 载入状态,fetch 里读写 state.storage,Worker 侧 idFromName 拿 stub 转发。真正需要想清楚的是切分粒度:ID 越集中,一致性越容易,吞吐越受限。

数据来源与检索时间

检索时间 2026-11-10。

来源URL对应数据
Durable Objects 概览https://developers.cloudflare.com/durable-objects/单实例语义、状态存储模型(未抓取)
DO 配置与迁移https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/new_sqlite_classes、tag 的写法(未抓取)
DO 存储 APIhttps://developers.cloudflare.com/durable-objects/api/storage-api/storage.get/put/delete/list 与 blockConcurrencyWhile(未抓取)

核验说明

  • 未部署,未本地运行。账号 token 只含 Workers 与 KV 的写权限,没有 Durable Objects 权限;本篇也没有用 wrangler dev 跑本地模拟,配置与代码未经执行验证。
  • 第 2 节的 migrations 写法、第 3 节的类结构、第 4 节的 idFromName 用法均依据官方文档的通用形态,未逐条比对原文(三页文档都没有抓取)。
  • 第 6 节是自测清单,不是实测结果。读者部署后按这几条核对,能覆盖大部分配置错误。
  • 关于免费档要求 SQLite 后端这一点,依据文档记忆,未在控制台核对,如遇部署报错请以报错文本为准。
  • 未做:没有测 alarm、没有测 WebSocket 与休眠、没有测 storage.sql、没有测实例迁移后的老数据兼容。