Hasura GraphQL Engine 元数据乐观并发控制(Optimistic Concurrency Control)设计解析

发布时间:2026/9/20 0:23:23
Hasura GraphQL Engine 元数据乐观并发控制(Optimistic Concurrency Control)设计解析 后端API网关数据库GraphQL【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址https://gitcode.com/gh_mirrors/gr/graphql-engine点击查看免费下载导读本文以 rfcs/optimistic-concurrency-control.md 为骨架深入剖析 Hasura GraphQL Engine 如何通过resource_version资源版本号机制解决多客户端并发修改元数据Metadata时的冲突问题多个用户或自动化脚本同时操作同一个实例时如何避免操作基于过期元数据执行导致的报错与副作用。读完本文你将理解该 RFC 的动机、v1/metadataAPI 的请求/响应契约演进、export_metadatav2 的引入以及该设计在hdb_catalog表与 schema 同步机制中的最终落地实现。背景控制台对外部元数据变更的失察Hasura 的元数据如表跟踪、关系、权限、事件触发器、远程 schema 等统一存储在服务器的hdb_catalog中任何客户端都可以通过/v1/metadataAPI 或控制台进行修改。RFC 指出现状存在一个核心痛点Console 无法感知发生在它之外例如同一项目的另一位用户的元数据变更。当用户试图修改一个已经在服务端被其他人改动过的对象时操作可能报错也可能造成非预期的副作用。RFC 给出了两个并发场景假设两个用户通过不同控制台页面操作同一台服务器场景 1操作直接报错用户 A 想取消跟踪untrack表t但在点击按钮之前用户 B 已经取消了表t的跟踪用户 A 的请求到达服务端后得到类似table t not found的错误。场景 2副作用悄悄丢失他人成果用户 A 想取消跟踪表t但在点击按钮之前用户 B 从表x到表t新增了一条关系relationship用户 A 的 untrack 请求依然执行成功但同时把用户 B 在表x上创建的关系一并丢弃了。场景 2 比场景 1 更隐蔽、更危险——它不是报错而是静默地覆盖了他人的修改。为了让控制台感知外部变更并在用户基于过期元数据操作时给出警告RFC 引入了乐观并发控制Optimistic Concurrency Control, OCC。方案核心resourceVersion版本号机制乐观并发控制是处理共享资源并发更新的经典策略。RFC 的核心思想可以概括为三步服务端维护内部版本号为元数据资源维护一个内部resourceVersion每次资源变更时递增客户端携带版本号客户端在修改操作中携带它所见过的资源的resourceVersion服务端校验版本号仅当客户端发送的resourceVersion与服务端当前值一致时操作才被放行从而保证资源自客户端上次查看以来未被修改。请求流程示例RFC 以场景 1 为例给出了带版本号的完整请求流。设元数据当前版本为v两个用户打开控制台时都拿到resourceVersion v用户 B 发送 untrack 表t的请求携带resourceVersion v服务端校验当前版本确为v放行操作并将resourceVersion递增到w随响应返回用户 A 对此毫不知情发出相同的 untrack 表t请求仍携带resourceVersion v服务端发现当前版本已是w比客户端携带的v更新于是拒绝请求并在响应中返回新的resourceVersion用户 A 的控制台据此提示用户元数据已在服务端被修改并引导其重新拉取元数据。这一机制的本质是乐观的它不假设冲突一定会发生而是在提交时一次性校验冲突时拒绝并让客户端基于最新版本重试。所需的 API 变更1. 为所有元数据操作传递resourceVersion当前所有元数据操作都以 POST 形式提交到v1/metadata请求体如下{ type: operation_name, version: version_of_the_operation, args: OperationArgs }RFC 给出了两种携带resourceVersion的方案方案 A扩展 JSON 请求体增加顶层键{ type: operation_name, version: version_of_the_operation, args: OperationArgs, resourceVersion: resourceVersion }方案 B作为 URL 查询参数POST /v1/metadata?resourceVersionx { type: operation_name, version: version_of_the_operation, args: OperationArgs }RFC 认为两种方案都合理可以选取实现成本更低的一种。同时特别注明部分操作应当忽略resourceVersion例如export_metadata——它只导出、不修改元数据不应因版本不匹配而被拒绝。从当前仓库的实现看最终采用的是方案 AJSON 请求体顶层键在 server/src-lib/Hasura/Server/API/Metadata.hs 中RQLMetadata数据类型通过o .:? resource_version从请求体解析出可选的_rqlMetadataResourceVersion类型为Maybe MetadataResourceVersion随请求一并传入执行流程。这意味着resource_version在实现中是可选字段——不携带它的请求依然会被处理向后兼容只有携带时才会触发版本校验。2.export_metadata响应变更与 v2 APIexport_metadata的响应将改为携带版本号{ resourceVersion: x, metadata: Metadata }由于响应结构发生了变化需要新增一个v2 版本的export_metadataAPI以免破坏既有客户端。当前仓库中该 API 已落地在 server/src-lib/Hasura/RQL/DDL/Metadata.hs 的runExportMetadataV2中响应被构造为包含resource_version与metadata两个字段的对象而在 server/src-lib/Hasura/Server/API/Metadata.hs 的runMetadataQueryV2M中可以看到当前 v2 元数据 API 仅支持两个操作RMV2ExportMetadata与RMV2ReplaceMetadata。3. 修改类操作的响应携带新版本号所有会修改元数据的操作其响应都需要包含修改后的新resourceVersion以便客户端更新本地记录。从实现看这一信息体现在服务端更新元数据后返回的新版本上详见下文实现细节中的写入流程。仓库中的落地实现从 RFC 到代码该 RFC 的设想已在当前仓库中完整实现以下从源码层面印证其关键环节。存储层hdb_metadata表中的版本列元数据与版本号存储在同一张表hdb_catalog.hdb_metadata中。在 server/src-lib/Hasura/RQL/DDL/Schema/Catalog.hs 中fetchMetadataAndResourceVersionFromCatalog通过SELECT metadata, resource_version FROM hdb_catalog.hdb_metadata同时读取元数据与版本fetchMetadataResourceVersionFromCatalog单独读取版本号。版本号类型定义在 server/src-lib/Hasura/RQL/Types/SchemaCache.hsMetadataResourceVersion是包装了Int64的 newtype初始版本为initialResourceVersion MetadataResourceVersion 0MetadataWithResourceVersion则将元数据与其版本打包携带。乐观并发校验ON CONFLICT ... WHERE resource_version $2RFC 中服务端仅当客户端版本与当前版本一致时才放行的语义在 setMetadataInCatalog 中实现得非常精巧——它借助 PostgreSQL 的INSERT ... ON CONFLICT原子语义完成条件更新 版本递增 校验失败返回 409INSERT INTO hdb_catalog.hdb_metadata(id, metadata) VALUES (1, $1::json) ON CONFLICT (id) DO UPDATE SET metadata $1::json, resource_version hdb_catalog.hdb_metadata.resource_version 1 WHERE hdb_catalog.hdb_metadata.resource_version $2 RETURNING resource_version若WHERE条件命中客户端携带的版本等于当前版本则更新元数据并递增版本返回新版本号若条件不命中则RETURNING返回空结果代码随即抛出 409 冲突错误metadata resource version referenced (...) did not match current version对应 Catalog.hs。WHERE resource_version $2正是 RFC 所描述的核心校验逻辑且整个校验 更新 递增在单条 SQL 中原子完成天然规避了并发窗口。请求分发哪些操作会触发版本校验在 server/src-lib/Hasura/Server/API/Metadata.hs 的runMetadataQuery中服务端先取出当前的MetadataWithResourceVersion执行操作后调用updateMetadataAndNotifySchemaSync写入路径见 server/src-lib/Hasura/App.hsnewResourceVersion - updateMetadataAndNotifySchemaSync appEnvInstanceId (fromMaybe currentResourceVersion _rqlMetadataResourceVersion) -- 客户端版本缺省用当前版本 modMetadata cacheInvalidations注意fromMaybe currentResourceVersion当请求未携带resource_version时直接以服务端当前版本参与校验等价于不做 OCC 检查保证了老客户端与脚本的兼容性——这与 RFC 中部分操作可忽略 resourceVersion的意图一致。同时同一文件中的queryModifiesMetadata函数Metadata.hs以穷举方式标注了每个元数据操作是否修改元数据export_metadata、get_inconsistent_metadata、introspect_remote_schema等只读操作返回False而untrack_table、replace_metadata、reload_metadata等全部返回True。只有返回True的操作才会触发写入目录 递增版本的流程这与 RFC 中某些操作如export_metadata不修改元数据、应忽略版本号的论断完全吻合。版本递增的另一条路径run_sql元数据级联RFC 的最后一条变更要求指出任何 source 上的run_sql若引发了元数据级联变更例如表重命名也必须递增resourceVersion。因为run_sql直接改变数据库 schema进而会级联更新 Hasura 的元数据如重命名表后关系、权限的级联调整这类变更同样属于元数据被外部修改。仓库中为此提供了独立的bumpMetadataVersionInCatalog函数Catalog.hs其实现为UPDATE hdb_catalog.hdb_metadata SET resource_version hdb_catalog.hdb_metadata.resource_version 1即当不需要修改元数据内容、仅需要版本递增以通知其他实例/客户端时使用该函数完成 bump。微妙之处SubtletiesRFC 提出的边界问题RFC 专门用一节讨论了版本号机制必须注意的边界情况这些考量对实现质量至关重要1.reload_metadata也必须递增版本reload_metadata本身不修改数据库中的元数据内容但 RFC 论证了它仍然必须 bump 版本理由有三影响不一致对象集合reload_metadata可能使元数据进入不一致状态。用户在控制台 1 看到x个不一致对象、正准备点击drop_inconsistent_metadata此时控制台 2 的另一个用户 reload 了元数据不一致对象变为y个。由于元数据内容未变若不 bump 版本控制台 1 的删除请求会顺利通过但实际上它基于的是过时的不一致状态改变操作语义reload_metadata会拉取远程 schema 的最新 schema。用户 1 正基于旧 schema 定义远程 schema 权限用户 2 reload 后 schema 已变化此时add remote schema permissions请求不应放行因为该权限是基于更早的 schema 定义的版本号作为多实例同步的候选机制团队一直在考虑用resourceVersion替代当前基于 listen/notify 的 schema 同步机制。若reload_metadata不 bump 版本这种方案将无法工作。从当前仓库看reload_metadata在queryModifiesMetadata中返回True见 Metadata.hs即它确实被纳入修改元数据的范畴并触发版本递增与该节的设计结论一致。2.run_sql的元数据级联必须递增版本如前所述run_sql引发的任何元数据级联如表重命名都必须 bumpresourceVersion否则其他客户端仍会基于旧版本操作重演本文开头场景 2 的静默覆盖问题。3. 关于多实例 schema 同步的延伸RFC 提到版本号机制与现有 schema 同步机制基于 listen/notify的关系。从仓库实现看二者已融合updateMetadataAndNotifySchemaSyncserver/src-lib/Hasura/App.hs在更新元数据后调用notifySchemaCacheSyncTx向hdb_catalog.hdb_schema_notifications写入带resource_version的通知而 server/src-lib/Hasura/Server/SchemaUpdate.hs 中的轮询同步逻辑通过fetchMetadataNotificationsFromCatalogCatalog.hsSELECT ... WHERE resource_version $1 AND instance_id ! $2按版本号增量拉取其他实例的变更通知并用setMetadataResourceVersionInSchemaCache更新引擎内的scMetadataResourceVersion定义于 server/src-lib/Hasura/RQL/Types/SchemaCache.hs。由此可见resourceVersion已成为跨实例 schema 同步的公共基准与 RFC 中的前瞻性设想保持一致。总结optimistic-concurrency-controlRFC 为 Hasura GraphQL Engine 的元数据并发安全设计了一条清晰的技术路线设计要点RFC 提议仓库落地版本载体服务端维护递增的resourceVersionhdb_catalog.hdb_metadata.resource_versionInt64初始 0请求携带方式JSON 顶层键或 URL 参数JSON 顶层可选键resource_versionAPI/Metadata.hs并发校验版本不一致则拒绝ON CONFLICT ... WHERE resource_version $2失败抛 409Catalog.hs导出带版本新增export_metadatav2runExportMetadataV2返回resource_versionmetadataRQL/DDL/Metadata.hs只读操作豁免不修改元数据的操作忽略版本queryModifiesMetadata穷举区分读写API/Metadata.hs边界 bumpreload_metadata、run_sql级联需递增reload_metadata归入写操作bumpMetadataVersionInCatalog独立递增Catalog.hs多实例同步版本号可作为 schema 同步基准hdb_schema_notifications按resource_version $1增量拉取Catalog.hs对于控制台与 API 客户端开发者而言这套机制给出了明确的集成范式操作前通过export_metadatav2获取resource_version所有修改类操作携带该值收到 409 冲突后重新导出元数据并提示用户刷新——这正是 RFC 期望的感知外部变更、避免静默覆盖的完整闭环。相关设计与实现可进一步参考 rfcs/optimistic-concurrency-control.md、server/src-lib/Hasura/Server/API/Metadata.hs 与 server/src-lib/Hasura/RQL/DDL/Schema/Catalog.hs。赞分享后端API网关数据库GraphQL【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址https://gitcode.com/gh_mirrors/gr/graphql-engine点击查看免费下载相关推荐Milvus Partial Update 乐观并发控制Optimistic CAS设计深度解析Milvus Partial Update 乐观并发控制Optimistic CAS设计深度解析 本文以仓库设计文档 docs/design docs/de数据库向量数据库分布式数据库后端Hasura GraphQL Engine v3 架构深度解析从 Open DDS 元数据到 GraphQL 执行引擎Hasura GraphQL Engine v3 架构深度解析从 Open DDS 元数据到 GraphQL 执行引擎 本文以 v3/docs/archite后端API网关数据库GraphQL数据库并发控制机制Awesome Design Patterns 乐观与悲观锁数据库并发控制机制Awesome Design Patterns 乐观与悲观锁 为什么需要并发控制 你是否遇到过这些问题电商秒杀时商品超卖转账操作导致余额文档技术博客创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询