在边缘构建有状态应用
Cloudflare Workers 很快,D1 很近,但真正困难的是把状态、一致性与失败边界设计清楚。

边缘计算最吸引人的承诺,是让代码离用户更近。但当应用开始保存账号、会话和留言,问题就不再只是“把函数部署到更多节点”,而是:状态究竟在哪里,谁对它负责,失败时系统会怎样退化?
这篇文章用一个带登录和留言的博客作为最小案例,梳理在 Cloudflare Workers 与 D1 上构建有状态应用时真正重要的设计边界。
先画清楚状态地图
一个看起来很小的博客,至少有三类完全不同的状态:
| 状态 | 来源 | 生命周期 |
|---|---|---|
| 文章正文 | Git 中的 Markdown | 随部署更新 |
| 用户与留言 | D1 | 长期持久化 |
| 登录会话 | D1 + Cookie | 有明确过期时间 |
文章内容适合跟着代码版本走。它需要评审、回滚和构建时校验,却不需要读者在运行时修改。用户数据则刚好相反:它必须脱离部署生命周期存在。
选择数据库之前,先判断一份数据应该跟随代码版本,还是跟随用户行为。
这个区分会自然导向一个简单架构:Nuxt Content 负责 Markdown,D1 负责运行时业务数据,Worker 负责把两者组合成一次请求。
绑定比网络请求更重要
Worker 访问 D1 时不需要自己保存 API Token,也不应该在运行时调用 Cloudflare REST API。D1 通过 binding 直接进入运行时:
{
"d1_databases": [
{
"binding": "DB",
"database_name": "signal-log-db",
"database_id": "<YOUR_DATABASE_ID>"
}
]
}
在服务端路由里,数据库对象来自当前请求上下文。每一条 SQL 都使用 prepared statement:
const comment = await db
.prepare(`
SELECT id, body, created_at
FROM comments
WHERE post_slug = ?1 AND status = 'published'
ORDER BY created_at DESC
LIMIT 100
`)
.bind(postSlug)
.all()
参数绑定不是代码风格问题,而是输入边界的一部分。文章路径、邮箱、昵称和留言正文都来自用户,任何一个值都不应该被拼接进 SQL。
会话只存不可逆摘要
登录成功后,浏览器拿到一个随机会话令牌;数据库只保存它的 SHA-256 摘要。这样即使会话表意外暴露,原始令牌也不能被直接拿去登录。
const token = createRandomToken()
const tokenHash = await sha256(token)
await db
.prepare('INSERT INTO sessions (token_hash, user_id, expires_at) VALUES (?1, ?2, ?3)')
.bind(tokenHash, userId, expiresAt)
.run()
Cookie 使用 HttpOnly,让前端 JavaScript 无法读取;使用 SameSite=Lax 降低跨站请求风险;生产环境再加上 Secure。
这并不意味着系统从此“绝对安全”。更准确的说法是:我们让每一层只持有它完成工作所需的最少能力。
一致性要围绕用户动作设计
D1 是关系数据库,但边缘应用仍然需要明确哪些流程要求强一致。
对博客来说,关键动作很少:
- 注册后应当立刻能够登录;
- 留言成功后应当立刻在当前页面出现;
- 退出后旧会话应当立刻失效。
这些流程都应该从同一个主数据库完成写入和紧随其后的读取。文章列表、公开留言列表等纯读取场景,才有更大的缓存空间。
不要从“哪里可以缓存”出发,而要从“用户刚刚完成了什么承诺”出发。
失败也需要产品设计
边缘运行时会失败,数据库也可能暂时不可用。区别只在于,我们有没有把失败变成用户能理解的状态。
- 登录失败时,不区分“邮箱不存在”和“密码错误”,避免暴露账号枚举信息;
- 留言写入失败时,保留输入内容,让用户可以重试;
- 内容查询失败时,返回真正的错误状态,而不是空白页面;
- 所有服务端错误使用结构化日志,但不把内部 SQL 或堆栈发给浏览器。
系统可靠性不是“永远不出错”,而是错误发生时边界仍然完整。
最后:让架构保持无聊
这个方案没有消息队列、没有分布式锁,也没有额外的身份服务。不是因为那些工具没有价值,而是当前问题还没有提出相应要求。
一个长期可维护的个人站,最好的基础通常是:
- 内容跟着 Git;
- 数据进入 D1;
- 密钥留在 Worker secret;
- 输入在边界验证;
- 失败清楚、可观察、可恢复。
当流量或产品需求真正变化时,再让架构跟着证据演进。边缘已经足够快,工程的任务是让它继续简单。
留言
登录后加入讨论
你的邮箱不会公开,留言只显示昵称。