SpacetimeDB 订阅语义详解:WebSocket 消息顺序、事务更新与客户端缓存一致性保证 📅 发布时间:2026/9/12 23:33:32 👁 浏览次数: SpacetimeDB 订阅语义详解WebSocket 消息顺序、事务更新与客户端缓存一致性保证【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDBSpacetimeDB 通过 WebSocket 为客户端提供数据库实时订阅能力客户端声明一组 SQL 查询服务端持续推送匹配行的插入、删除与更新使客户端内存中的缓存始终与数据库保持同步。本文基于 订阅语义官方文档结合仓库源码WebSocket v2 协议定义、订阅计划实现、订阅 Actor与 TypeScript SDK 实现系统讲解订阅期间的消息排序保证、订阅建立与事务更新两条工作流、多订阅集合的捆绑机制以及客户端缓存的一致性模型。读完本文你将能够正确理解并可靠使用 SpacetimeDB 的订阅 API避免在并发场景下踩中读取到不一致缓存的陷阱。一、通信模型一条 WebSocket 连接两条消息通道SpacetimeDB 客户端与宿主host之间的一条 WebSocket 连接上存在两个方向互不干扰的消息通道客户端 → 服务端Client → Server发送请求包括 reducer 调用CallReducer、订阅查询Subscribe、取消订阅Unsubscribe、一次性查询OneOffQuery与过程调用CallProcedure。服务端 → 客户端Server → Client发送对客户端请求的响应以及数据库事务产生的更新推送。这一点在 WebSocket v2 消息定义 中得到了直接印证ClientMessage枚举包含Subscribe、Unsubscribe、OneOffQuery、CallReducer、CallProcedure五种客户端消息而ServerMessage枚举则包含InitialConnection、SubscribeApplied、UnsubscribeApplied、SubscriptionError、TransactionUpdate、OneOffQueryResult、ReducerResult、ProcedureResult八种服务端消息ServerMessage 定义。协议层还有一个关键设计每条客户端消息都携带一个客户端自行编号的request_id服务端对request_id不赋予任何语义但会在对应的响应中原样带回。客户端据此将请求与响应一一关联v2.rs 中的 request_id 说明。二、三类消息排序与原子性保证服务端对订阅相关的消息流维持以下三条核心保证1. 顺序响应保证Sequential Response Ordering对客户端请求的响应永远按照请求到达的顺序发回。若请求 A 先于请求 B 到达那么 A 的响应必然先于 B 的响应到达客户端——即使 A 实际执行耗时更长。这意味着客户端无需为响应与请求的对应关系引入复杂的乱序重排逻辑。2. 原子事务更新保证Atomic Transaction Updates每一个数据库事务reducer 调用、INSERT/UPDATE/DELETE语句等最多生成零个或一个更新消息下发到客户端。这些更新是原子的且严格反映事务提交的先后顺序。换句话说客户端不会收到某个事务的半套更新也不会观察到跨事务交错的数据。这一点与 v2.rs 中TransactionUpdate的文档注释 完全一致一个事务如果没有影响客户端的任何订阅查询集客户端将收不到空的TransactionUpdate即零个只有相关订阅集受影响时才会收到包含相应QuerySetUpdate的更新即一个。3. 原子订阅初始化保证Atomic Subscription Initialization订阅建立时客户端恰好收到一个包含全部初始匹配行的响应。该响应基于夹在两个事务之间的一致性数据库状态快照快照是一个已提交的数据库状态它包含客户端此前已收到的所有事务更新它排除所有尚未发生未来的事务更新。换言之初始快照永远不会让客户端先看到半截状态也不会与后续增量更新产生时间上的空洞或重叠。三、订阅建立工作流Subscribe 流程当客户端 SDK 调用SubscriptionBuilder::subscribe(QUERIES)时完整流程分四步订阅语义文档 的 Subscription Workflow 一节客户端 SDK → 宿主发送包含查询集合的Subscribe消息。消息体包含三要素——request_id请求标识、query_set_id该订阅集合在连接内唯一的标识由客户端分配、query_strings一组 SQLSELECT语句Subscribe 结构定义。宿主处理捕获已提交数据库状态的一致性快照将查询集合对该快照求值确定匹配行。宿主 → 客户端 SDK发送SubscribeApplied消息携带request_id、query_set_id与匹配行QueryRows按表组织的SingleTableRows列表SubscribeApplied 定义。客户端 SDK 处理锁定客户端缓存将全部初始行原子地一次性插入随后触发相关回调——每行触发on_insert订阅集合整体触发on_applied。服务端发送SubscribeApplied的实现位于 module_subscription_actor.rs 的 add_multi_subscription_inner宿主编译查询compile_queries后在持有数据库锁的前提下将订阅注册进subscriptions表并通过broadcast_queue.send_client_message_v2把SubscribeApplied发给对应客户端。注意回调顺序无保证。文档明确指出on_insert与on_applied之间不保证相对调用顺序。因此客户端代码不应假设on_applied一定在所有on_insert之后或相反。订阅计划的编译从 SQL 到增量维护片段SubscribeApplied的初始快照求值只是订阅生命周期的一半另一半是持续增量维护。在 crates/subscription/src/lib.rs 中SubscriptionPlan::compile_plans通过compile_subscription把每条 SQL 编译为物理计划并拆解为两类执行片段Fragmentsinsert_plans计算需要插入订阅视图的行delete_plans计算需要从订阅视图删除的行。对单表SELECT而言各只需一个片段对join则各需要四个片段。文档事务结果是一个状态增量unordered set of inserted/deleted rows的表述正对应了源码中Delta::Inserts/Delta::Deletes的增量扫描机制。值得注意的两条编译期约束源码中为bail!直接拒绝订阅 join 的连接列必须建有索引Subscriptions require indexes on join columns事件表event table不能作为订阅 join 的查找表。源码中还提供JoinEdge剪枝优化当存在形如SELECT a.* FROM a JOIN b ON a.id b.id WHERE b.x ?的多条订阅时一旦a发生变化只重新求值受影响的查询而非全量重算lib.rs 中 JoinEdge 注释。四、事务更新工作流TransactionUpdate 流程当数据库提交一个事务后服务端执行以下步骤事务产生状态增量事务的结果是一个状态增量state delta——一组无序的、被插入与被删除的行集合。注意这里并不包含整行修改的概念一次对某行的UPDATE会被拆解为删除旧行 插入新行在协议层体现为PersistentTableRows中并列的inserts与deletes列表参见 TableUpdateRows 定义。宿主对增量求值将各订阅查询针对该状态增量求值确定受影响的行。宿主 → 客户端 SDK若存在相关更新发送包含受影响行与事务元数据的TransactionUpdate消息。其结构为query_sets: Box[QuerySetUpdate]每个QuerySetUpdate又按表分组为TableUpdate最终落到PersistentTableRows { inserts, deletes }或EventTableRows { events }TransactionUpdate 结构。客户端 SDK 处理锁定客户端缓存原子地应用删除与插入随后触发相关回调——被修改的行触发on_insert、on_delete、on_update若事务源于 reducer还会触发对应的 reducer 回调。再次强调上述回调之间同样不保证相对调用顺序文档中的第二处:::note。服务端侧提交并广播发生在 commit_and_broadcast_event。该函数在提交事务之前就对subscriptions表获取读锁源码注释明确指出否则订阅者可能收到重复更新提交并降级事务后再评估各订阅查询集的变化把更新消息写入广播队列。事务失败EventStatus::FailedUser等则走回滚分支不会向客户端广播任何部分更新——这正是原子事务更新保证的落地实现。增量视图维护的数学基础订阅在本质上是一个表/join 的物化视图lib.rs 中的 Fragments 注释 给出了增量维护的完整推导设V为时刻t的 join 视图V为时刻t1的视图则V V ∪ dv其中dv是视图的增量对两表 joindv()需插入与dv(-)需删除分别由四个片段构成Rds()、dr()S、dr()ds(-)、dr(-)ds()以及对应的删除项。这套推导的意义在于宿主无需在每次事务后重算整个 join只需基于 delta增量计算受影响的部分从而把订阅维护的开销与事务变更规模成正比而非与表规模成正比。五、多订阅集合的捆绑更新当同一连接上存在多个订阅集合query sets时一次事务产生的、影响这些集合的所有更新会捆绑进同一条TransactionUpdate消息下发。从 TransactionUpdate 结构 可以看到TransactionUpdate内部就是query_sets: Box[QuerySetUpdate]——每个元素以query_set_id区分属于哪个订阅集合。客户端按query_set_id将各集合的变更路由到对应的订阅处理逻辑。捆绑发送的好处是一次事务的多个集合变更具备整体原子性客户端可以一次性、一致地应用全部变更再统一触发回调。六、客户端缓存一致性保证订阅的最终目的是让客户端维护一个本地镜像。文档明确了四条缓存保证缓存永远一致客户端缓存始终维护已提交数据库状态的正确子集consistent and correct subset不会出现多出来的行或缺失的行。回调可见完整状态因事件触发的回调函数保证能看到完全更新后的缓存状态。缓存读取零成本读取客户端缓存的成本近似于零因为它访问的是本地缓存数据这也是订阅模式相比逐条请求的价值所在。回调期间状态精确在回调执行期间客户端缓存精确反映触发该事件的事务提交之后的数据库状态。待定回调机制Pending Callbacks与缓存一致性最后一条保证尤其关键它的实现依赖待定回调机制处理一条TransactionUpdate消息时SDK 会把回调排队queued并延迟执行直到该事务的缓存更新插入/删除完全应用完毕。这确保所有回调看到的都是完全一致的缓存状态杜绝回调观察到不一致的中间状态。该机制在 TypeScript SDK 中有清晰对应SubscriptionBuilderImpl的文档注释指出on_applied回调标记的区间与缓存更新边界精确对应subscription_builder_impl.ts回调队列在缓存变更全部落定后才逐一派发。因此在回调内部读取任何已订阅表的缓存都不会读到更新到一半的数据——例如一个将行从表 A 移动到表 B 的事务回调期间不会出现行已从 A 删除但尚未插入 B的可观测状态。七、订阅生命周期中的错误处理与取消订阅语义不止覆盖成功路径协议还定义了完整的失败与取消流程v2.rs 中的协议注释初始失败若查询无效或编译/求值失败服务端返回SubscriptionError此时request_id为Some不发送SubscribeApplied。应用后失败若订阅已在运行、但在某次增量求值时失败如重编译失败服务端会发送request_id为None的SubscriptionError。客户端收到后应丢弃该query_set_id之前收到的全部行并不再期待后续更新。取消订阅客户端发送Unsubscribe携带request_id与query_set_id服务端回UnsubscribeApplied确认。若设置了SendDroppedRows标志确认消息中还会携带需要从客户端缓存移除的完整行集UnsubscribeFlags 定义。标识复用无论订阅成功、失败还是被取消一旦服务端不再引用某个query_set_id该 ID 即可由客户端复用。query_set_id只在单个连接的ConnectionId范围内有意义并非全局唯一v2.rs 中 Subscribe 字段注释。八、实践要点总结基于以上语义在使用 SpacetimeDB 订阅时值得固化的几个结论无需实现乱序重排请求响应严格按序事务更新原子且有序客户端可放心依赖消息到达顺序。回调内缓存必然一致所有on_insert/on_delete/on_update/ reducer 回调执行时缓存已完整应用对应事务可直接安全读取。不要依赖回调相对顺序同一事务内多个回调包括on_insert与on_applied之间的先后无保证跨行的顺序性逻辑应在缓存数据上自行推导而非依赖回调调用次序。UPDATE 表现为删除插入若需区分修改注意协议层PersistentTableRows只有inserts与deletes两个列表on_update由 SDK 依据主键匹配推导而来。正确响应SubscriptionError收到错误后必须清理该query_set_id的本地缓存行方可安全复用该 ID。订阅查询要建索引join 订阅的连接列必须建索引否则编译期即被宿主拒绝。以上语义是 SpacetimeDB 客户端 SDK 各语言实现Rust / TypeScript / C# / C 等统一遵循的契约掌握了消息顺序、原子性快照、增量更新与待定回调这四块基石就能在多客户端并发写入的实时应用里写出正确、可预期的订阅逻辑。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考