GraphQL Subscriptions
GraphQL Subscription 让客户端声明并持续接收某类服务端事件,解决当前客户端没有亲自发起变化时的实时同步;它与 Apollo normalized cache、WebSocket/SSE transport、共享 PubSub 与可靠事件恢复属于不同层次。
GraphQL Subscriptions
一句话定义
Subscription 是 GraphQL 的实时 operation:客户端声明自己订阅哪类数据变化,服务端在事件发生后持续推送符合 Schema 的结果。
subscription {
diagnosisUpdated(userId: "u1") {
id
summary
updatedAt
}
}
它表达的是“订阅什么数据”,而不是规定底层必须使用哪一种网络传输。
与 Query / Mutation 的关系
Query
= 请求一次当前数据
Mutation
= 当前客户端主动发起一次显式写操作
Subscription
= 持续接收服务端发生的数据变化
三者共享同一套 GraphQL Schema 和字段选择模型。
为什么自己发起 Mutation 时通常不需要 Subscription
假设 Client A 自己执行:
mutation {
updateTask(id: "123", input: { status: DONE }) {
id
status
}
}
服务端返回:
Task:123
status = DONE
Apollo Client 可以根据 Mutation response 中的 entity identity 把自己的 normalized cache 更新:
Client A Mutation
→ Server update
→ Mutation response
→ Apollo cache merge
→ Client A UI 更新
因此 Client A 通常不需要再靠 Subscription 听到“你刚才自己改了 Task 123”。
注意:这依赖响应足以描述受影响状态。列表 membership、派生字段、多实体副作用等场景仍可能需要手动 cache reconciliation 或 refetch。
Subscription 真正解决什么问题
更典型的是:当前客户端没有亲自发起这次变化。
例如:
用户 A / 设备 A / 后台 Job / Agent
→ 修改 Task 123
用户 B 的页面
→ 没有发起这次 Mutation
→ 本地 cache 不会凭空知道服务器已经变化
此时可以:
Server publishes taskUpdated
↓
Subscription
↓
Client B 收到 Task:123 新状态
↓
Apollo normalized cache merge
↓
Client B UI 更新
所以一个很实用的区分是:
Mutation reconciliation 解决“我自己写完以后,本地怎么跟上”;Subscription 解决“别人、别的设备或服务器自己改了以后,我怎么及时知道”。
单用户也可能需要实时同步
“只有一个账号/用户”不等于所有变化都由当前页面亲自发起。
仍可能有:
- 同一用户多个 tab;
- 手机和电脑同时在线;
- 后台任务改变状态;
- Agent / Worker 完成异步任务;
- 定时任务或 webhook 更新数据;
- 另一个服务修改共享状态。
因此判断是否需要 Subscription,关键不是“有几个用户”,而是:
当前客户端是否能从自己已经收到的 response 中可靠知道这次服务端状态变化。
Subscription 不等于 WebSocket
GraphQL Subscription 是应用层语义;WebSocket 是常见 transport。
GraphQL Subscription
↓
subscription protocol / execution
↓
WebSocket(常见)或其他 transport
所以:
“我订阅什么”与“字节怎样持续传输”是两个不同问题。
与 SSE 的关系
SSE 更接近一个单向 server → client transport:
HTTP connection
↓
server events
↓
client
GraphQL Subscription 更关注:
Schema 中声明订阅字段
↓
客户端选择字段
↓
服务端按 GraphQL 结果形状推送
因此二者不是简单竞争关系。
GraphQL Subscription 更自然的场景
- 系统本身已经以 GraphQL 为主要 API;
- 实时数据也是 Schema 数据图的一部分;
- 客户端希望继续使用 GraphQL 类型、字段选择和 cache integration。
SSE 更自然的场景
- 单向服务端推送足够;
- AI token/event streaming;
- 事件本身已经有稳定 event envelope;
- 希望直接利用 HTTP 流式响应和简单重连模型。
BodySense 的 Agent streaming 属于后者:text delta / tool / state / safety 是运行时事件,不需要为了“实时”就强行包装成 GraphQL Subscription。
服务端怎样产生 Subscription 事件
常见逻辑:
Mutation / Worker / Domain Event
↓
业务状态更新成功
↓
publish event
↓
Subscription subscribers
Subscription Resolver 通常不是不停轮询数据库,而是消费某种 Pub/Sub 或事件流,再把事件映射成 GraphQL Schema 定义的 payload。
为什么单机内存 PubSub 在多实例下不够
假设:
Client A ─WebSocket→ Instance A
Client B ─Mutation → Instance B
如果 Instance B 只执行:
in-memory PubSub.publish(taskUpdated)
那么消息只存在于 Instance B 自己的进程内存。
Instance A 并不知道发生了事件:
Instance B memory PubSub
X
Instance A memory PubSub
于是连接在 Instance A 的 Client A 收不到更新。
多实例部署通常需要共享消息基础设施:
Instance B
↓ publish
Redis Pub/Sub / NATS / Kafka / message broker
↓
Instance A
↓
Subscription connection
↓
Client A
Kafka 可以承担这个职责,但简单的低延迟广播并不必然需要 Kafka;Redis Pub/Sub、NATS 等可能更轻量。具体选择取决于是否需要持久化、顺序、回放、吞吐与交付语义。
Subscription 与 Apollo Normalized Cache 的分工
这是最容易混淆的一组概念:
Subscription
→ 解决“远端变化怎样来到当前客户端”
Normalized Cache
→ 解决“数据来到以后怎样识别实体、合并并通知 UI”
例如:
Subscription payload:
{ __typename: "Task", id: "123", status: "DONE" }
↓
Apollo identify
↓
Task:123
↓
merge status = DONE
↓
引用 Task:123 的 UI 更新
实时传输和本地缓存 reconciliation 是前后两个阶段。
Subscription 仍然没有自动解决恢复
连接断开之后仍要回答:
漏掉的事件怎么办?
如何知道最后收到哪里?
重复事件如何去重?
连接恢复是否等于业务 run 恢复?
GraphQL Subscription 本身不会替你自动创造 durable event log、cursor、sequence 或 idempotency。
这和你已经学过的 BodySense 原则一致:
transport connection
!= durable business execution
如果业务要求可靠补发/回放,可能需要 durable event log、cursor/sequence、消息队列或重新 Query authority,而不仅是一个在线 WebSocket。
核心判断
Subscription 解决“用 GraphQL 描述并接收当前客户端之外的实时变化”,WebSocket/SSE 解决“怎样持续传输”,Normalized Cache 解决“收到数据后怎样合并”,共享 broker 解决“事件怎样跨实例传播”,durable event log/cursor 解决“断线以后怎样可靠恢复”。
Related notes
- 所属 MOC: GraphQL MOC
- 前置概念: GraphQL、GraphQL Schema
- 客户端缓存: GraphQL Normalized Cache
- Apollo Client: Apollo Client 在 GraphQL 中的角色
- 实时通信: 前后端实时通信
- 对比: GraphQL vs REST