AI 参与说明(Agent:Grok):本文由 Grok 根据 Convex 与 Cloudflare Durable Objects 官方文档、Context7 检索结果及站内已校验专题辅助调研、撰写和校验。结论按 2026-09-08 的公开资料整理。运行记录:模型
grok-4.6,提供方xAI,执行入口 Grok Build TUI。产品能力、限额与 API 会变化,落地时应以一手文档和项目 lockfile 为准。
Convex 和 Durable Objects 不是同一类产品,不能按“哪一个数据库更好”来二选一。 Convex 是带实时订阅的应用后端:一份可跨表查询的 document-relational database,加上 query / mutation / action。Durable Objects 是 Workers 上的 Actor:每个实例全球唯一、单线程、计算与私有存储同机,用来协调一个聊天室、一场预订或一个租户,而不是承载整份可查询业务库。
已经把业务事实和客户端状态放在 Convex 上时,继续用 Convex 的事务与订阅,不要把表拆进无数个对象。已经把入口放在 Cloudflare Workers,并且问题是“同一实体的请求必须串行协调”时,用 Durable Objects;需要跨实体报表、搜索或关系查询时,在 Cloudflare 侧应评估 D1 或 Hyperdrive,而不是把 Durable Objects 当成全局 SQL 库。两者可以并存:Convex 负责可查询事实与响应式 UI,Durable Objects 负责边缘上的房间、在线状态和锁。
资料核验于 2026-09-08。下文限额取自当时的官方 limits 页,实施前应再核当前计划与区域。
阅读前先看这几个词
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| Convex | 保留原名(产品名称) | 以 TypeScript 函数读写的响应式后端,含数据库、订阅与调度 |
| Durable Objects | 保留原名(产品名称) | Cloudflare Workers 上带身份、带私有存储的有状态实例 |
| query | 查询函数 | Convex 中只读、可订阅的服务端函数 |
| mutation | 变更函数 | Convex 中整段作为事务执行的读写函数 |
| action | 动作函数 | Convex 中可访问网络等副作用、但不能直接当事务写库的函数 |
| OCC | 乐观并发控制 | 先执行再在提交时检查读集是否仍是最新版本;冲突则重跑确定性事务 |
| serializable isolation | 可串行化隔离 | 并发事务的最终效果等同于按某一顺序依次执行 |
| Actor | 演员模型 | 每个实例独立收消息、单线程处理、自带私有状态 |
| DurableObjectNamespace | Durable Object 命名空间 | Worker 里按名字或 ID 找到某一个对象实例的入口 |
| stub | 存根 | 调用某个 Durable Object 实例的客户端句柄 |
| WebSocket Hibernation | WebSocket 休眠 | 空闲时对象可被驱逐出内存,连接仍在,有消息再唤醒 |
| Alarm | 闹钟 | 每个 Durable Object 实例最多一个、到期后至少执行一次的定时唤醒 |
| Input Gate | 输入门 | 同步执行期间拦住新事件,避免存储读写被其他请求插入 |
| Output Gate | 输出门 | 等存储写入落盘后才把响应发给调用方 |
Convex is a reactive document-relational database. Queries are TypeScript functions that subscribe to their read set; mutations are ACID transactions with serializable isolation and optimistic concurrency control; actions exist only for non-deterministic work such as fetch. The Convex Cloud persistence layer is PlanetScale MySQL; self-hosted backends can use SQLite, Postgres, or MySQL. Convex Overview
A Durable Object is a globally unique, single-threaded Worker with colocated storage. Cloudflare routes every request for a given ID to one live instance; that instance owns private SQLite (or legacy KV), optional hibernatable WebSockets, and one alarm. Storage is strongly consistent inside the object and invisible to other objects. Cloudflare positions this product for coordination, not as a general-purpose application database. What are Durable Objects? Rules of Durable Objects
先分清要解决的问题
把 Durable Objects 理解成“Cloudflare 版 Convex”,会同时用错两边。Convex 卖的是 一份大家都能查询的业务事实;Durable Objects 卖的是 一个能被精确找到的协调者。
flowchart TB
subgraph convexSide["Convex"]
C1["客户端 useQuery / mutation"] --> C2["同步协议与函数运行时"]
C2 --> C3["共享 document-relational database"]
C3 --> C4["读集失效后重跑 query"]
C4 --> C1
end
subgraph doSide["Durable Objects"]
D1["客户端或 Worker"] --> D2["DurableObjectNamespace 按业务键路由"]
D2 --> D3["某一个对象实例"]
D3 --> D4["私有 SQLite / 内存 / Alarm / WebSocket"]
end
左边的数据默认对整个应用可见:一条 mutation 可以同时改用户、订单和库存,订阅了这些表的客户端会一起更新。右边的数据默认只对当前实例可见:room-123 里的 messages 表和 room-456 里的同名表不是同一份数据,也没有内置的跨对象 JOIN。
Cloudflare 自己把 Durable Objects 标成 “global coordination & stateful serverless”,把 D1 标成轻量 SQL 库。若目标是“在 Cloudflare 上找一个更像应用数据库的东西”,对照对象是 D1 或 Hyperdrive,不是 Durable Objects。Choose a data or storage product
一张表看清边界
| 维度 | Convex | Durable Objects |
|---|---|---|
| 产品定位 | 应用后端:数据库 + 服务端函数 + 客户端订阅 | Workers 平台上的有状态协调原语 |
| 数据可见范围 | 部署内共享表,可用 index 跨文档查询 | 每个实例一份私有存储,其他实例不能直接读 |
| 一致性 | 数据库 ACID;mutation 提供 serializable isolation,冲突时按 OCC 重试 | 实例内强一致;计算与存储同机;跨实例没有分布式事务 |
| 并发模型 | 多 mutation 并行,靠 OCC 检测读写冲突;热点文档会打满重试 | 单线程。Input Gate / Output Gate 保护存储;外部 fetch 期间仍可能交错 |
| 实时 | query 订阅读集,写入后失效并重跑,客户端接到同一逻辑时间点 | 同一实例上的 WebSocket 可广播;没有跨对象的响应式查询引擎 |
| 调度 | ctx.scheduler 写入数据库;从 mutation 调度与事务原子提交;scheduled action 至多一次 | 每个实例一个 Alarm,至少一次执行,失败可能重试 |
| 位置 | Convex Cloud 按区域部署在 PlanetScale MySQL 上;不是按用户就近创建实例 | 通常在首次请求附近创建,可用 locationHint 建议区域 |
| 水平扩展 | 共享库 + 部署档并发上限;热点要拆文档或使用 Component | 无限个实例,但单个实例约 1000 rps 软上限;必须按协调原子分片 |
| 客户端 | React、移动端等一等客户端,useQuery 直接订阅 | 经 Worker 的 RPC / fetch / WebSocket 访问;没有跨对象的订阅客户端 |
| 外部 I/O | 只能放在 action;query / mutation 必须确定性 | 实例里可以直接 fetch,但会打开 Input Gate,不能把它当成自动事务 |
存储形态不要对错号
Convex 文档把库称为 document-relational:JSON 文档进表,表之间用 ID 表达关系,查询是 TypeScript 而不是 SQL。Convex Overview
Durable Objects 的推荐存储是 SQLite。Cloudflare 说明:D1 是托管数据库,应用代码与 SQL 通常隔着网络;SQLite in Durable Objects 是更底层的 “compute with storage”,必须同时写路由 Worker 和对象本身,换来的是逻辑可以紧贴这份私有库。SQL in Durable Objects vs D1
因此:
- 用户资料、订单、权限、后台报表 → Convex 或 D1 / Hyperdrive。
- 某个房间的连接、某张票的串行预订、某个租户的工作区锁 → Durable Objects。
- 把 Convex 里所有表按用户拆进 Durable Objects,会失去跨用户查询,却得不到 Convex 的订阅引擎。
一致性:OCC 重试 vs 单线程串行
两边都能正确完成“扣库存、订座位”这类经典问题,但正确性边界不同。
Convex:整段 mutation 是事务
Convex 规定整个 mutation 就是事务,没有 BEGIN / COMMIT。query 与 mutation 不能发网络请求;平台在 OCC 冲突时自动重跑这段确定性代码。官方用转账说明:读集版本仍是最新才提交,否则重来,不需要业务自己处理“合并冲突”。隔离级别是 serializable isolation,而不是只保证 snapshot isolation。OCC and Atomicity Convex Overview
代价也写在官方错误页里:同一文档被高频写入时,内部重试耗尽会抛 Write conflict: Optimistic concurrency control。修复方向是少读、少写热点文档,或改数据模型;这不是锁超时,而是共享库上的争用。Errors and Warnings
// 示例:说明 mutation 事务边界,未连接真实部署。
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const bookSeat = mutation({
args: { eventId: v.id("events"), seatId: v.string(), userId: v.id("users") },
handler: async (ctx, { eventId, seatId, userId }) => {
const existing = await ctx.db
.query("bookings")
.withIndex("by_event_seat", (q) =>
q.eq("eventId", eventId).eq("seatId", seatId),
)
.unique();
if (existing !== null) {
throw new Error("Seat already booked");
}
await ctx.db.insert("bookings", { eventId, seatId, userId });
},
});
这段代码可以同时碰到 events、bookings、users 上的相关文档;提交成功后,所有读到这些文档的 query 会失效重跑。它不序列化“整个活动”的所有请求,而是序列化 相交的读写集。
Durable Objects:一个活动一个实例
Cloudflare 的订座示例把同一 eventId 路由到同一个实例,然后在该实例的 SQLite 里检查并插入。官方把这称为协调:同一资源的请求进入同一单线程实例,从而避免超卖。Rules of Durable Objects
单线程不等于跨 await 的整段请求自动成事务。Input Gate 会在同步 JavaScript 和受保护的存储操作期间拦住新事件;await fetch() 会打开这扇门,其他请求可以交错。没有夹杂 await 的连续存储写会被合成一次隐式事务;KV 风格的 get / put 之间插入 await 会拆开这次提交。Output Gate 则保证调用方看到成功时,写入已经落盘。Rules of Durable Objects
// 示例:说明按 eventId 路由到单一实例,未连接真实 Worker。
import { DurableObject } from "cloudflare:workers";
export interface Env {
BOOKING: DurableObjectNamespace<SeatBooking>;
}
export class SeatBooking extends DurableObject<Env> {
async bookSeat(seatId: string, userId: string) {
const existing = this.ctx.storage.sql
.exec("SELECT user_id FROM bookings WHERE seat_id = ?", seatId)
.toArray();
if (existing.length > 0) {
return { success: false as const, message: "Seat already booked" };
}
this.ctx.storage.sql.exec(
"INSERT INTO bookings (seat_id, user_id, booked_at) VALUES (?, ?, ?)",
seatId,
userId,
Date.now(),
);
return { success: true as const, message: "Seat booked successfully" };
}
}
它保证的是 这个活动对象内部 的串行与落盘,不保证“改完座位后再原子更新另一份全局用户积分表”。跨对象更新要自己设计补偿或把需要原子的数据放进同一个对象。
实时:读集订阅 vs 房间内广播
Convex 的实时不是把行推给浏览器。query 在同一逻辑时间戳上读取,运行时记录读集;mutation 提交后,相交的订阅失效,服务端重跑 query,客户端库把多个订阅对齐到数据库的同一时刻。Queries Convex Overview
因此后台列表、权限过滤、聚合结果都可以直接 useQuery。action 的返回值不会自动进入这条链:外部结果要先经 mutation 写入,相关 query 才会更新。站内源码导读把这条失效路径写得更细,见 Convex 源码导读:开源仓库版图、核心架构与阅读路线。
Durable Objects 的实时是连接挂在同一个实例上。WebSocket Hibernation 允许对象在空闲时离开内存,连接仍保持;新消息到来再跑 constructor 并进入 webSocketMessage。这适合聊天室、游戏房间、协作光标:广播范围就是当前实例上的连接。What are Durable Objects?
它不提供“任意 SQL 的响应式订阅”。要在管理后台列出所有房间的未读数,需要额外把摘要写到 D1、Analytics Engine 或其他可查询存储;只查询各个对象的私有 SQLite 做不到一次扫描。
调度、位置与扩展
Convex 把 scheduled function 存在数据库里。从 mutation 里调用 ctx.scheduler.runAfter() / runAt() 与这次事务一起提交或一起回滚。scheduled mutation 保证执行一次;scheduled action 因为有副作用,官方语义是 at most once,瞬时错误不会自动再跑。Scheduled Functions
Durable Objects 的 Alarm 属于单个实例:同时只有一个,到期后至少执行一次,处理失败可能重试,因此 alarm() 必须可重入。它适合“这个房间 30 秒后结算”“这个租户的订阅到期”,不适合替代全局 cron。Alarms 站内接口说明见 Durable Objects 使用方式与接口整理。
位置上,Convex Cloud 按区域跑在 PlanetScale MySQL 上,EU 区域有独立定价,不是“用户在哪实例就在哪”。Durable Objects 会在首次请求附近创建,并可用 locationHint;官方也写明尚未覆盖所有数据中心。Convex Overview What are Durable Objects?
扩展上两边都会在“热点”上显形,只是热点形状不同:
| 热点 | Convex | Durable Objects |
|---|---|---|
| 形态 | 同一文档或同一窄 index 被大量 mutation 同时写入 | 过多请求打进同一个实例 |
| 官方信号 | OCC write conflict;部署档还有并发 mutation 上限,例如 S16 为 16 | 单个实例约 1000 rps 软上限,过载返回 overloaded |
| 拆法 | 拆热点文档、使用 sharded counter 一类 Component | 按房间、比赛、租户拆实例,禁止全局单例 |
Convex 免费/入门档是 S16:同时运行的 query 16 个、mutation 16 个;Professional 的 S256 把这两项提到 256。SQLite-backed Durable Objects 在 Workers Paid 上对象数量不限,单个对象存储 10 GB,Free 计划单个对象 1 GB。这些数字会变,引用时以 limits 页为准。Convex Limits Durable Objects Limits
What are Durable Objects? 页脚注仍写过 “SQLite 目前 1 GB、GA 升到 10 GB”。2026-09-08 核对 limits 页与 2026-08-25 changelog 时,Workers Paid 已按每对象 10 GB 记录;以 limits 页为准,不要沿用过期脚注。Durable Objects Limits
三个场景怎么选
1. 订一座位
两边都能做对。Convex 适合座位还要出现在“我的订单”、库存报表、支付状态这些跨表事实里,一次 mutation 可以同时写 bookings 和 orders。Durable Objects 适合高峰时所有预订必须打到同一协调者,而且不一定需要立刻做跨活动查询。
若选 Durable Objects,仍可能要把“已售出”的摘要写进 D1 或 Convex,供列表页查询。对象内存放权威座位图,可查询库放只读投影。
2. 聊天室
Durable Objects 更贴房间模型:连接、顺序、广播都在一个实例上,WebSocket Hibernation 降低空闲成本。Convex 也能存消息并用 query 订阅房间历史,这在“关页后再打开看到完整记录”的产品里很完整;但它不是把 TCP 连接钉在某个房间进程上。站内已有的 Agent 对比说明:Cloudflare Agents SDK 的会话状态就落在 Durable Object SQLite 上。Convex Agent Component 与 Cloudflare Agents SDK
需要消息可搜索、可审核、可跨房间统计时,不要只留在某个对象的私有表里。
3. 管理后台与多租户业务库
这是 Convex 明显更省事的区域:权限过滤后的列表、跨租户聚合、关系型业务规则,都可以写成 query / mutation。Durable Objects 官方明确反对一个全局对象扛全部请求,也反对把所有数据塞进一个对象;分层数据应拆成子对象,跨对象查询要另找存储。Rules of Durable Objects
在 Cloudflare 栈里做这类后台,更接近 D1,而不是“更多的 Durable Objects”。
什么时候可以一起用
作者当前优先技术栈同时包含 Convex 与 Cloudflare 全家桶,所以“选一个扔掉另一个”不是默认答案。有意义的组合是职责分开,而不是两份真相。
flowchart TB
UI["TanStack Start 或 React 客户端"] --> Worker["Cloudflare Worker:认证与路由"]
Worker --> ConvexDB["Convex:用户、订单、权限、可查询事实"]
Worker --> Room["Durable Objects:房间、在线状态、串行锁"]
ConvexDB --> UI
Room -->|"WebSocket 或 RPC"| UI
Room -->|"摘要、审计、投影"| ConvexDB
较稳的切法:
- 权威业务事实在 Convex。 账户、订单、角色、计费状态用
mutation保证跨表原子,用query驱动 UI。 - 协调原子在 Durable Objects。 房间连接、短时锁、回合制状态机、需要贴着实例的 WebSocket。
- 对象写回投影,而不是反向当主库。 房间结束、座位确认后,用 Worker 调 Convex
mutation(或 Queue / Workflow)落下可查询记录。不要让管理后台去 fan-out 扫描所有对象。 - 不要用 Durable Objects 替代 Convex 订阅。 列表页继续走 Convex;对象只推“此刻这个房间里正在发生的事”。
已经采用 Workers + TanStack Start、且产品主要是实时房间或 Agent 会话时,不必仅为关系型主库再引入 Convex;这时 Durable Objects 加 D1 往往更贴平台。已经采用 Convex、只是偶发需要一把边缘锁或一个 WebSocket 房间时,可以为那一类实体加 Durable Objects,而不要把整个 schema 搬迁过去。
编排层的对照不在本文展开:Convex Workflow 与 Cloudflare Workflows 解决的是持久化步骤,不是对象身份。见 Convex Workflow、Cloudflare Workflows 与 Dynamic Workflows。
决策顺序
- 需要一份全应用可查询、可订阅的业务库吗?是则先选 Convex;若人已经在 Workers 上且不想引入第二套后端,评估 D1 / Hyperdrive,而不是 Durable Objects。
- 需要把同一实体的连接或写入协调到全球唯一实例吗?是则用 Durable Objects,并按房间、文档、租户、库存项分片。
- 两者都需要时,让 Convex(或 D1)保存可查询事实,让 Durable Objects 保存协调状态,用显式投影连接它们。
- 只有一个全局计数器、一把全局锁、一张全局表时,两边都要改模型:Convex 会 OCC 冲突,Durable Objects 会 overloaded。先拆热点,再谈产品选择。
关联阅读
- Durable Objects 使用方式与接口整理:身份、stub、SQLite、Alarm 与 WebSocket 接口。
- Durable Object Facets:概念、使用场景与 Durable Objects 对比:动态子代码如何在父对象下获得隔离存储。
- Convex 源码导读:开源仓库版图、核心架构与阅读路线:query 读集、OCC 与订阅失效如何串起来。
- Convex Agent Component 与 Cloudflare Agents SDK:关页后继续生成并恢复聊天:会话 ACK 与 Durable Object SQLite 在 Agent 场景中的取舍。
- Convex Workflow、Cloudflare Workflows 与 Dynamic Workflows:区别、场景与使用方式:持久化编排与本文的状态原语不是同一层问题。
- Convex + TanStack Query:
useQuery与useSuspenseQuery的选择、原理与场景:Convex 订阅进入 TanStack Query 时的边界。
参考资料
- Convex Overview
- OCC and Atomicity
- Queries:Caching, Reactivity, Consistency
- Mutations
- Actions
- Scheduled Functions
- Convex Limits
- Convex Errors:Write conflict
- What are Durable Objects?
- Rules of Durable Objects
- Choose a data or storage product
- Durable Objects Limits
- SQLite-backed Durable Object Storage
- Alarms
- Durable Objects: Easy, Fast, Correct — Choose three