ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Cloudflare Durable Objects 存储实战模式:Schema 迁移、缓存、限流与批处理的完整实现指南

Cloudflare Durable Objects 存储实战模式:Schema 迁移、缓存、限流与批处理的完整实现指南 Cloudflare Durable Objects 存储实战模式Schema 迁移、缓存、限流与批处理的完整实现指南【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills本指南以 Cloudflare Deploy Skill 仓库中 do-storage/patterns.md 为核心系统讲解 Durable ObjectsDO持久化存储的九大实战模式Schema 迁移、内存缓存、限流、Alarm 批处理、初始化、安全计数、父子协调、写合并与清理。读完本文你将掌握 DO StorageSQLite 推荐后端在生产环境中的完整使用姿势包括并发门控Input/Output Gate原理、事务规则与性能边界可直接套用到计数器、会话、实时协作、限流器等真实场景。前置认知DO Storage 的两类后端与三套 API在展开模式之前先明确 Durable Objects 的持久化底座。do-storage/README.md 指出DO Storage 提供SQLite推荐与KV遗留两种后端对应三套 API后端创建方式可用 API30 天 PITR 恢复SQLite推荐new_sqlite_classes迁移SQL 同步 KV 异步 KV✅KV遗留new_classes迁移仅异步 KV❌SQL APIctx.storage.sql完整 SQLite含 FTS5 全文检索、JSON、数学函数扩展同步 KVctx.storage.kv仅 SQLite 后端可用性能更高异步 KVctx.storage两类后端通用。本文所有模式均默认基于 SQLite 后端编写。配套配置、API 与陷阱清单分别见 configuration.md、api.md 与 gotchas.md。并发模型理解所有模式的前提Durable Objects 单线程串行处理请求但其防竞态的关键是Input/Output Gate输入/输出门控机制见 gotchas.mdInput 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 的migrationsnew_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 Mapstring, User(); async getUser(id: string): PromiseUser | undefined { if (this.cache.has(id)) { const cached this.cache.get(id); if (cached) return cached; } const user await this.ctx.storage.getUser(user:${id}); if (user) this.cache.set(id, user); return user; } async updateUser(id: string, data: PartialUser) { 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.mdDO 空闲会自动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): Promiseboolean { 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 实例避免单点过载删除语句可以不同步awaitInput 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.mdAlarm 是可靠调度器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(): Promisenumber { 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(): PromiseResponse { let val await this.ctx.storage.get(counter); this.ctx.storage.put(counter, val 1); // 无 await return new Response(String(val)); // 输出门保证响应在写入确认后才发出 }两种写法都安全但语义不同写法一返回Promisenumber调用方拿到的是本次递增前的原值适合需要「唯一序号」的分配场景如订单号写法二返回Response通过不await写操作 输出门延迟响应来优化延迟——省去一次微任务等待同时不牺牲一致性务必警惕这种「无 await 写」的捷径只在门控完整时成立。一旦代码中出现fetch()对外请求输入/输出门即被打破请求可穿插必须改用blockConcurrencyWhile()包裹关键区见 gotchas.md更彻底的方案是原子 SQLINSERT ... 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): Promisestring { 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(): Promisestring[] { 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.mdRPC 调用仓库 configuration.md 指出modern RPCcompatibility_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 Statementsasync 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.mdasync 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.mdDO 存储按 GB-month 计费长期不用的命名实例应主动deleteAll()释放容量SQLite 后端每对象 10 GB 上限、KV 后端不限量但 KV key 2 KiB / value 128 KiB限额表见 gotchas.md需要恢复到历史点时使用 PITR APIgetCurrentBookmark()/getBookmarkForTime()/onNextSessionRestoreBookmark()this.ctx.abort()重启实例仅 SQLite详见 api.md。落地清单选型、限额与验证将上述模式落地到生产时对照以下检查点后端选型新项目一律走 SQLitenew_sqlite_classes获得 SQL 同步 KV PITR仅存量迁移保留 KVnew_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),仅供参考
返回列表