durable object 替代 kv D1 数据库并实现访问加速
下面这两篇博客很清楚地解释了 durable object的相关概念,包括输出门,内建的缓存,原子性,竞态条件的处理以及 sqlite 作为存储端。
https://blog.cloudflare.com/durable-objects-easy-fast-correct-choose-three/
https://blog.cloudflare.com/sqlite-in-durable-objects/
为什么要使用 durable object
为了加速cloudflare-memoflow的访问速度,通过测试发现瓶颈在 kv 键值的访问速度,而 D1 数据库则比较快,我可以使用 D1 数据库来替代 kv 存储。但是我还发现更好的方式 Durable Object (持久性存储)
简而言之,你应该将 D1 视为一个更“管理型”的数据库产品,而 SQLite-in-DO 则更像是低级别的“存储与计算”构建块。
持久化对象需要更多努力,但作为回报,赋予你更多权力。使用 DO,你拥有两段在不同地方运行的代码:一个前端 Worker,它将来自互联网的请求路由到正确的 DO,以及 DO 本身,它在与 SQLite 数据库相同的机器上运行。你可能需要仔细考虑哪些代码在哪里运行,你可能需要构建一些 D1 中现成的工具。但因为你完全控制,你可以根据应用程序的需求定制解决方案,并可能实现更多
因为代码与sqlite 数据库在一块所以,与数据库相关的增删改查的代码可以不考虑网络延迟,代码会更简洁。
cloudflare memoflow 添加 durable object 的 commit 记录
page 调用 durable 的代码
add durable call
https://github.com/qyzhizi/hono-auth-test-app-D1-func/commit/3a75eab60efaa358bb6dface566a8ed7df4807ee
https://github.com/qyzhizi/hono-auth-test-app-D1-func/commit/01ac8014d19a6ae6078dcc0e0790e4cc216e1c8d
worker durable 的代码
https://github.com/qyzhizi/hono-auth-test-app-D1/commit/0bac45b584f0337b90bb252e0459e0ab3b9d0e49
wrangler types 生成 types 文件
"generate-types": "wrangler types", 记录 Cloudflare 的类型文件生成, wrangler types
会生成 types 文件:worker-configuration.d.ts
package.json
"wrangler": "^4.13.2"
"wrangler": "^4.13.2" 生成的 worker-configuration.d.ts 包含了更多的东西,需要卸载之前的安装的包:"@cloudflare/workers-types"
"wrangler": "^3.60.3" 的配置:
package.json
"devDependencies": {
"@cloudflare/workers-types": "^4.20240925.0",
"typescript": "^5.5.2",
"wrangler": "^3.60.3"
}
生成 worker-configuration.d.ts
后还需要配置文件 tsconfig.json
的 types 字段,否则会报错。
"types": ["./worker-configuration.d.ts", "node"],
下面这个命名是指定了 生成的文件名:env.d.ts
wrangler types --env-interface CloudflareEnv env.d.ts
这个命令生成的 env.d.ts
文件不需要配置 tsconfig.json
的 types 字段,似乎 ts 编译器可以自动读取 env.d.ts 文件
Cloudflare durable object 迁移配置在迁移完成后,配置是否可以去掉旧的 tag
简短答案:可以
参考:https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
Cloudflare durable object 迁移配置 :
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"MyDurableObject"
]
},
{
"tag": "v2",
"renamed_classes": [
{
"from": "MyDurableObject",
"to": "MemoflowDurableObject"
}
],
迁移完成后,配置是可以去掉 tag v1
// —— 迁移配置 ——
"migrations": [
// {
// "tag": "v1",
// "new_sqlite_classes": [
// "MyDurableObject"
// ]
// },
{
"tag": "v2",
"renamed_classes": [
{
"from": "MyDurableObject",
"to": "MemoflowDurableObject"
}
],
// "deleted_classes": [
// "MyDurableObject"
// ]
}
],
"durable_objects": {
"bindings": [
{
"class_name": "MemoflowDurableObject",
"name": "MemoFlow_DURABLE_OBJECT"
}
]
},
"observability": {
"enabled": true
}
cloudflare 官方博客 对 Durable Object 的介绍,比较详细,重点解释了如何处理竞态条件与加速访问
https://blog.cloudflare.com/durable-objects-easy-fast-correct-choose-three/
cloudflare debug page and worker in a single Miniflare instance #cloudflare #durable
在一个 Miniflare 中同时启动一个 page 与一个worker
npx wrangler pages dev -c wrangler.jsonc -c /Users/qy/Documents/git_rep/cloudflare_project/2025-03-05-hono-github-login-db/hono-auth-test-app-D1/wrangler.jsonc
这个命令表示先启动 当前目录的 page, 配置文件是 wrangler.jsonc
, 接着 -c /Users/qy/Documents/git_rep/cloudflare_project/2025-03-05-hono-github-login-db/hono-auth-test-app-D1/wrangler.jsonc
表示附加启动另一个 worker -c
接另一个 worker 的目录,这样的好处是 在同一个 Miniflare 实例中同时同时启动 一个 page 和一个 worker,使得 page 可以访问另一个 worker 中的 Durable object。如果在命令行中分别启动 page 与 worker,page 则会显示无法访问 durable object。
分别启动
npx wrangler pages dev -c wrangler.jsonc
npx wrangler dev -c wrangler.jsonc
参考:
https://github.com/cloudflare/workers-sdk/pull/7251
https://github.com/cloudflare/workers-sdk/pull/7098
durable object starter 网站
https://developers.cloudflare.com/durable-objects/get-started/?utm_source=chatgpt.com#3-instantiate-and-communicate-with-a-durable-object
部署的网站:
https://durable-object-starter.l2830942138.workers.dev
Cloudflare pages 不能创建 Durable object
You must create a Durable Object Worker and bind it to your Pages project using the Cloudflare dashboard or your Pages project's Wrangler configuration file. You cannot create and deploy a Durable Object within a Pages project.
DO 只是接收 来自 Worker 或者 另一个 DO 的请求
Durable Objects do not receive requests directly from the Internet. Durable Objects receive requests from Workers or other Durable Objects.
记录 durable object 存放数据失败的原因,数据校验出错,自己给自己设置绊脚石
错误的代码:
private isTaskPayload(data: any): data is Partial<Task> {
// 你的校验逻辑,比如检查 commitMessage/content 类型
return (
typeof data === 'object' &&
(data.id === undefined || data.id === 'string') &&
(data.commitMessage === undefined || typeof data.commitMessage === 'string') &&
(data.content === undefined || typeof data.content === 'string') &&
(data.completed === undefined || typeof data.completed === 'boolean')
)
}
更改后:
private isTaskPayload(data: any): data is Partial<Task> {
// 你的校验逻辑,比如检查 commitMessage/content 类型
return (
typeof data === 'object' &&
(data.id === undefined || typeof data.id === 'string') &&
(data.commitMessage === undefined || typeof data.commitMessage === 'string') &&
(data.content === undefined || typeof data.content === 'string') &&
(data.completed === undefined || typeof data.completed === 'boolean')
)
}
改动:
data.id === 'string'
-> typeof data.id === 'string'
类型校验报错的地方:!this.isTaskPayload(data)
async createTask(data: Task): Promise<Task> {
if (!this.isTaskPayload(data)) {
throw new ValidationError('Invalid payload');
}
// const id = crypto.randomUUID();
const task: Task = {
id: data.id ,
commitMessage: data.commitMessage ?? '',
completed: false,
createdAt: new Date().toISOString(),
content: data.content ?? '',
filePath: data.filePath ?? ''
};
await this.state.storage.put(data.id, task);
return task;
}
我一开始 Invalid payload 以为是第三方的报错,由于是 AI 写的,没想到是自己写的代码的错误信息
我怀疑是 durable object 本身的问题,后来经过本地调试,发送调用 另一个 durablehello的函数就没有问题,最终找到这个错误的位置
durable 删除任务 是没有返回值的,之前在这里犯了这个错误 #durable
export const durableDeleteTask = async (c: Context, id: String): Promise<Task> => {
const doId = c.env.MemoFlow_DURABLE_OBJECT.idFromName('MemoflowDO')
const stub = c.env.MemoFlow_DURABLE_OBJECT.get(doId)
console.log("delete task, id: ", id)
const task: Task = await stub.deleteTask(id)
if (!task) {
throw new NotFoundError(`durableDeleteTask with id=${id} failed`)
}
return task
}
上面的代码及时删除成功也会抛出错误,就是因为 task 原本就是 null 值,改为下面的代码,通过捕获错误来判断是否执行成功
export const durableDeleteTask = async (c: Context, id: string): Promise<void> => {
const doId = c.env.MemoFlow_DURABLE_OBJECT.idFromName('MemoflowDO');
const stub = c.env.MemoFlow_DURABLE_OBJECT.get(doId);
try {
await stub.deleteTask(id);
} catch (error) {
// 如果 Durable Object 内部已经抛出 NotFoundError,这里可以捕捉再处理或继续抛出
if (error instanceof NotFoundError) {
throw error;
}
console.error("Unexpected error during deleteTask:", error);
throw new Error(`Failed to delete task with id=${id}`);
}
};
A worker how to access another worker durable object
wrangler.toml 配置文件
The Durable Objects docs mention using script_name (the name of the script where the Durable Object class was defined, exported and published) to access a Durable Object class from another script.
[durable_objects]
bindings = [{name = "EXAMPLE_CLASS", class_name = "DurableObjectExample", script_name = "example-name"}]
page wrangler.jsonc 配置文件
// 1. Durable Object 绑定
"durable_objects": {
"bindings": [
{
"class_name": "MemoflowDurableObject",
"name": "MemoFlow_DURABLE_OBJECT",
"script_name": "memoflow_worker"
}
]
},
https://community.cloudflare.com/t/sharing-durable-objects-between-workers-locally/503413