Cloudflare Durable Objects 存储实战模式:Schema 迁移、缓存、限流与批处理的完整实现指南
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
本指南以 Cloudflare Deploy Skill 仓库中 do-storage/patterns.md 为核心,系统讲解 Durable Objects(DO)持久化存储的九大实战模式:Schema 迁移、内存缓存、限流、Alarm 批处理、初始化、安全计数、父子协调、写合并与清理。读完本文,你将掌握 DO Storage(SQLite 推荐后端)在生产环境中的完整使用姿势,包括并发门控(Input/Output Gate)原理、事务规则与性能边界,可直接套用到计数器、会话、实时协作、限流器等真实场景。
前置认知:DO Storage 的两类后端与三套 API
在展开模式之前,先明确 Durable Objects 的持久化底座。do-storage/README.md 指出,DO Storage 提供SQLite(推荐)与KV(遗留)两种后端,对应三套 API:
| 后端 | 创建方式 | 可用 API | 30 天 PITR 恢复 |
|---|---|---|---|
| SQLite(推荐) | new_sqlite_classes迁移 | SQL + 同步 KV + 异步 KV | ✅ |
| KV(遗留) | new_classes迁移 | 仅异步 KV | ❌ |
- SQL API(
ctx.storage.sql):完整 SQLite,含 FTS5 全文检索、JSON、数学函数扩展; - 同步 KV(
ctx.storage.kv):仅 SQLite 后端可用,性能更高; - 异步 KV(
ctx.storage):两类后端通用。
本文所有模式均默认基于 SQLite 后端编写。配套配置、API 与陷阱清单分别见 configuration.md、api.md 与 gotchas.md。
并发模型:理解所有模式的前提
Durable Objects 单线程串行处理请求,但其防竞态的关键是Input/Output Gate(输入/输出门控)机制(见 gotchas.md):
- Input Gate(输入门):当前请求在存储读取期间,阻塞其他请求进入,保证「读-改-写」不被穿插;
- Output Gate(输出门):当前请求的所有写入确认落盘后,才放行响应返回客户端。
两条铁律贯穿本文所有模式:写操作不必须await(输出门兜底),读操作前必须保证门控生效。此外注意,fetch()会打破输入/输出门导致请求穿插,此时必须用blockConcurrencyWhile()或transaction()显式保护。这个「为什么可以这样写」的底层原理,是理解下文每个模式的关键。
模式一:Schema 迁移——用 SQLiteuser_version做版本化演进
DO 的 SQLite 存储没有独立的迁移框架,官方推荐直接用 SQLite 内建的user_versionPRAGMA 记录 schema 版本,在构造函数中按版本逐级升级:
export class MyDurableObject extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.sql = ctx.storage.sql; // 使用 SQLite 内建 user_version pragma 记录当前版本 const ver = this.sql.exec("PRAGMA user_version").one()?.user_version || 0; if (ver === 0) { this.sql.exec(`CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT)`); this.sql.exec("PRAGMA user_version = 1"); } if (ver === 1) { this.sql.exec(`ALTER TABLE users ADD COLUMN email TEXT`); this.sql.exec("PRAGMA user_version = 2"); } } }该模式将「建表」「加列」等结构变更与版本号绑定,逐级叠加。设计要点:
- 幂等与顺序:每个版本只执行一次,通过
ver判断当前所处阶段,天然支持旧实例升级到新 schema; - 与平台迁移区分:此处是对象内数据库表结构的演进;而 DO 类的创建/改名/删除需要借助 wrangler.jsonc 的
migrations(new_sqlite_classes、renamed_classes、deleted_classes),二者职责不同,详见 configuration.md; - 验证方式:仓库 testing.md 展示了如何用
vitest-pool-workers在测试中查询_meta表断言 schema 版本,迁移逻辑可被自动化测试覆盖。
模式二:In-Memory Caching——存储之上叠一层内存缓存
DO 实例在存续期间拥有可用的内存堆,可在 KV/SQL 之上做读缓存,减少存储访问:
export class UserCache extends DurableObject { cache = new Map<string, User>(); async getUser(id: string): Promise<User | undefined> { if (this.cache.has(id)) { const cached = this.cache.get(id); if (cached) return cached; } const user = await this.ctx.storage.get<User>(`user:${id}`); if (user) this.cache.set(id, user); return user; } async updateUser(id: string, data: Partial<User>) { const updated = { ...await this.getUser(id), ...data }; this.cache.set(id, updated); await this.ctx.storage.put(`user:${id}`, updated); return updated; } }⚠️ 必须牢记的边界:DO 的Map属于易失内存。根据 durable-objects/gotchas.md,DO 空闲会自动Hibernation(休眠)或被Eviction(驱逐),内存中的cache全部丢失;构造函数在每次唤醒(冷启动或休眠唤醒)都会重新执行。因此:
- 内存缓存只做读加速,所有关键数据必须已落盘(本模式中
getUser在缓存未命中时回源存储); - 写路径必须双写:既更新
cache,又await写回存储,避免内存与持久层分叉; - 若需要在休眠/驱逐后恢复每连接状态,应使用 WebSocket 的
serializeAttachment()(见 durable-objects/gotchas.md)。
模式三:Rate Limiting——SQLite 计数实现的滑动窗口限流
利用 SQLite 存储每实例强一致的特点,在单个 DO 内实现限流(这也是 DO 最经典的命名实例协调场景之一):
export class RateLimiter extends DurableObject { async checkLimit(key: string, limit: number, window: number): Promise<boolean> { const now = Date.now(); // 清理窗口外过期记录 this.sql.exec('DELETE FROM requests WHERE key = ? AND timestamp < ?', key, now - window); // 统计当前窗口内请求数 const count = this.sql.exec('SELECT COUNT(*) as count FROM requests WHERE key = ?', key).one().count; if (count >= limit) return false; // 记录本次请求 this.sql.exec('INSERT INTO requests (key, timestamp) VALUES (?, ?)', key, now); return true; } }实现思路是「滑动窗口计数」:先删除窗口外的旧记录,再统计窗口内计数并决定是否放行。需要注意:
- 限流键的分片:单个 DO 的吞吐软上限约 1K req/s(见 gotchas.md 限额表)。高流量场景应使用
idFromName(identifier)按用户/租户分片到不同 DO 实例,避免单点过载; - 删除语句可以不同步
await:Input Gate 保证本请求内的读写不被穿插,配合输出门完成落盘确认; - 计数结果在并发请求下依然正确,正是依赖 DO 单线程串行 + 门控的语义。
模式四:Batch Processing with Alarms——用单个 Alarm 做延迟批量刷盘
DO 每个实例仅支持一个 Alarm(见 durable-objects/README.md 的「Rules of Durable Objects」),但可通过「攒批 + 单 Alarm」模式实现高效的批量写入:
export class BatchProcessor extends DurableObject { pending: string[] = []; async addItem(item: string) { this.pending.push(item); // 无 Alarm 时才设置,5 秒后统一处理 if (!await this.ctx.storage.getAlarm()) await this.ctx.storage.setAlarm(Date.now() + 5000); } async alarm() { const items = [...this.pending]; this.pending = []; // 单条多行 INSERT 批量落库 this.sql.exec(`INSERT INTO processed_items (item, timestamp) VALUES ${items.map(() => "(?, ?)").join(", ")}`, ...items.flatMap(item => [item, Date.now()])); } }要点拆解:
- 节流语义:
getAlarm()判空后setAlarm(),保证 5 秒窗口内多次addItem只触发一次 Alarm,把高频小写入合并为低频批量写入; - 批量 SQL:动态拼接多行
INSERT ... VALUES (?, ?), (?, ?), ...,一次执行替代 N 次写入,显著降低rowsWritten计费与 IO 开销(计费按请求、GB-month、rowsRead/rowsWritten 计算,见 README.md); - Alarm 是可靠调度器:
setAlarm()持久化于存储,DO 被驱逐后 Alarm 仍会触发;不能用setTimeout替代(内存定时器会在驱逐时丢失,见 durable-objects/gotchas.md); - 测试支撑:仓库 testing.md 提供
runDurableObjectAlarm()帮助函数,可在单测中手动触发 Alarm 并断言processed_items表行数。
模式五:Initialization Pattern——用blockConcurrencyWhile安全初始化
构造函数会在每次唤醒时执行(冷启动或休眠唤醒),此时其他请求可能同时到达。用blockConcurrencyWhile阻塞并发请求,确保初始化完成后再服务:
export class Counter extends DurableObject { value: number; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); // 初始化期间阻塞其他请求进入 ctx.blockConcurrencyWhile(async () => { this.value = (await ctx.storage.get("value")) || 0; }); } async increment() { this.value++; this.ctx.storage.put("value", this.value); // 不 await(输出门保护落盘确认) return this.value; } }两个关键细节:
- 读用
await,写不用await:读取必须等待结果才能计算新值;写入在blockConcurrencyWhile之外不await也安全,因为 Output Gate 会延迟响应直到写入确认(对应 api.md 中allowUnconfirmed选项背后的语义); - 初始化要轻:构造函数每次唤醒都执行,不要在构造中加载重型数据,应使用懒加载模式(首次访问时才读取),见 durable-objects/gotchas.md。
模式六:Safe Counter / Optimized Write——读-改-写与无 await 写入的两种姿势
针对计数器这类「读-改-写」操作,patterns.md 给出两种写法:
// 写法一:读改写在 Input Gate 保护下串行执行 async getUniqueNumber(): Promise<number> { let val = await this.ctx.storage.get("counter"); // Input gate 阻塞其他请求 await this.ctx.storage.put("counter", val + 1); return val; } // 写法二:写不 await,交给 Output Gate 在响应前确认 async increment(): Promise<Response> { let val = await this.ctx.storage.get("counter"); this.ctx.storage.put("counter", val + 1); // 无 await return new Response(String(val)); // 输出门保证响应在写入确认后才发出 }两种写法都安全,但语义不同:
- 写法一返回
Promise<number>,调用方拿到的是本次递增前的原值,适合需要「唯一序号」的分配场景(如订单号); - 写法二返回
Response,通过不await写操作 + 输出门延迟响应来优化延迟——省去一次微任务等待,同时不牺牲一致性; - 务必警惕:这种「无 await 写」的捷径只在门控完整时成立。一旦代码中出现
fetch()(对外请求),输入/输出门即被打破,请求可穿插,必须改用blockConcurrencyWhile()包裹关键区(见 gotchas.md); - 更彻底的方案是原子 SQL:
INSERT ... ON CONFLICT DO UPDATE SET value = value + 1 RETURNING value,一步完成读改写(见 README.md 的 Quick Start)。
模式七:Parent-Child Coordination——父 DO 协调子 DO 的层级架构
当业务需要「工作区/文档」「租户/成员」这类层级结构时,用父 DO 持有元数据、子 DO 承载实体数据:
// 父 DO 负责创建与跟踪子 DO export class Workspace extends DurableObject { async createDocument(name: string): Promise<string> { const docId = crypto.randomUUID(); // 由父 DO ID + docId 派生子 DO 的确定性 ID const childId = this.env.DOCUMENT.idFromName(`${this.ctx.id.toString()}:${docId}`); const childStub = this.env.DOCUMENT.get(childId); await childStub.initialize(name); // 在父 DO 存储中登记子文档元数据 this.sql.exec('INSERT INTO documents (id, name, created) VALUES (?, ?, ?)', docId, name, Date.now()); return docId; } async listDocuments(): Promise<string[]> { return this.sql.exec('SELECT id FROM documents').toArray().map(r => r.id); } } // 子 DO 承载单个文档的内容 export class Document extends DurableObject { async initialize(name: string) { this.sql.exec('CREATE TABLE IF NOT EXISTS content(key TEXT PRIMARY KEY, value TEXT)'); this.sql.exec('INSERT INTO content VALUES (?, ?)', 'name', name); } }该模式的价值:
- ID 派生:子 DO 的
idFromName参数包含父 DO 的id.toString(),保证命名空间隔离且无需额外注册表; - 解耦扩展:父 DO 只存索引/元数据,单个文档的海量数据落到各自子 DO,天然规避单 DO 10 GB 存储与 ~1K req/s 吞吐的软上限(gotchas.md);
- RPC 调用:仓库 configuration.md 指出,modern RPC(compatibility_date ≥ 2024-04-03)可直接
await stub.someMethod()调用子 DO 方法,类型安全且无需 HTTP 语义;需要完整 HTTP 语义(header/status)时才回退到stub.fetch()。
模式八:Write Coalescing——写合并与原子批处理
同一 key 的多次写入会自动原子合并(last write wins),这是输出门的直接红利:
async updateMetrics(userId: string, actions: Action[]) { // 同一 key 的多次写合并为一次原子提交——无需逐个 await for (const action of actions) { this.ctx.storage.put(`user:${userId}:lastAction`, action.type); this.ctx.storage.put(`user:${userId}:count`, await this.ctx.storage.get(`user:${userId}:count`) + 1); } // 输出门保证所有写入在响应前完成确认 return new Response("OK"); }⚠️ 一个需要小心的例外:循环内的get仍需await(依赖其返回值计算新值),且要注意 gotchas.md 的警告——同一事件内用Promise.all()并发发起多个存储操作不受 Input Gate 保护,会触发 "Race Condition in Concurrent Calls" 错误;门控只串行化不同事件间的请求,不串行化同一事件内的并发操作。
对需要更强原子性的多行写入,改用 SQL 事务(不要直接写BEGIN TRANSACTION,必须走事务 API,否则报 "Direct SQL Transaction Statements"):
async batchUpdate(items: Item[]) { this.sql.exec('BEGIN'); for (const item of items) { this.sql.exec('INSERT OR REPLACE INTO items VALUES (?, ?)', item.id, item.value); } this.sql.exec('COMMIT'); }同步场景推荐ctx.storage.transactionSync(),异步场景用ctx.storage.transaction()(后者支持ctx.storage.rollback()显式回滚),完整签名见 api.md。
模式九:Cleanup——释放对象与账号配额
资源回收是生产环境最容易遗漏的一环。清理需区分两类操作(patterns.md + api.md):
async cleanup() { await this.ctx.storage.deleteAlarm(); // 独立于 deleteAll,需显式删除 await this.ctx.storage.deleteAll(); // SQLite 后端原子清空;Alarm 不在其内 }deleteAlarm()必须先于deleteAll():deleteAll()只清数据,不会自动删除 Alarm。若先deleteAll()而 Alarm 残留,已清空的实例可能被 Alarm 再次唤醒执行alarm()处理空批,见 gotchas.md 的 "Alarm Not Deleted with deleteAll()" 条目;- 计费视角(README.md):DO 存储按 GB-month 计费,长期不用的命名实例应主动
deleteAll()释放容量;SQLite 后端每对象 10 GB 上限、KV 后端不限量但 KV key 2 KiB / value 128 KiB(限额表见 gotchas.md); - 需要恢复到历史点时使用 PITR API:
getCurrentBookmark()/getBookmarkForTime()/onNextSessionRestoreBookmark()+this.ctx.abort()重启实例(仅 SQLite,详见 api.md)。
落地清单:选型、限额与验证
将上述模式落地到生产时,对照以下检查点:
- 后端选型:新项目一律走 SQLite(
new_sqlite_classes),获得 SQL + 同步 KV + PITR;仅存量迁移保留 KV(new_classes); - 迁移生命周期:
migrations每次部署只执行一次,已有实例在下次调用时切换后端;重命名/删除类必须补renamed_classes/deleted_classes(注意deleted_classes会立即销毁数据,部署前用--dry-run验证,见 durable-objects/gotchas.md); - 限额意识(gotchas.md):单表最多 100 列、单行/单值最大 2 MB、SQL 语句最大 100 KB、参数最多 100 个、SQLite 每对象 10 GB、单 DO 吞吐软上限约 1K req/s——超限即分片或换 D1 共享库;
- 大整数陷阱:JavaScript number 只有 53 位精度,Snowflake/Twitter 这类 64 位 ID 必须存 TEXT,否则静默截断损坏;
- CPU 限制:默认 30s,可在 wrangler.jsonc 通过
limits.cpu_ms提到 300s(配置见 configuration.md); - 测试覆盖:用
@cloudflare/vitest-pool-workers的runInDurableObject/runDurableObjectAlarm对并发递增、Alarm 批处理、事务回滚、PITR 恢复逐项验证(testing.md 附完整示例)。
这九种模式覆盖了 DO 存储从「结构演进」到「性能优化」再到「资源治理」的完整闭环。建议按仓库给出的阅读顺序深入:先 configuration.md 完成环境搭建,再对照本文模式编写业务逻辑,遇到并发/精度问题回到 gotchas.md 排查,并用 testing.md 固化回归测试。
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考