#TypeScript#DevOps

缓存 Revalidation 正确性:Generation Fence、Singleflight 与陈旧写拒绝

Singleflight 能合并同时回源,却阻止不了旧重建者在失效后覆盖新值。本文用目标代际、重建租约、条件发布和有界 stale 策略封闭缓存击穿与迟到写窗口。

本文要点

  • 主题聚焦:TypeScript、DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年5月28日·更新于 2026年7月20日

Web 分层缓存策略说明 Cache-Control、CDN 与应用缓存如何分工,Redis 常见模式介绍互斥重建和击穿。真正棘手的竞态发生在“只允许一个回源者”之后:重建者 A 读取旧数据库快照,失效事件推进后重建者 B 写入新值,A 最后醒来又把旧值覆盖。Singleflight 只合并同一进程同一时刻的调用,分布式锁过期也不等于旧 owner 失去写能力。

缓存身份不只有业务 key

发布 cache.materialization.v1,把逻辑 slot 与不可变 value 分开。slot 由 tenant/auth scope、可枚举 dependency namespace、projection schema、查询参数摘要和策略版本组成;namespace 控制记录保存单调 requiredGeneration,slot 保存最近 committed generation/value digest、fresh/stale 期限与状态。动态集合查询绑定 collection/projection namespace,而不是要求源事务枚举任意 query digest。value envelope 还绑定 source snapshot、依赖 generation 向量、生成器版本和创建时间。

text
DependencyNamespace(scope, resourceOrCollection, projection)
  requiredGeneration, authEpoch
CacheSlot(namespace, queryDigest, policy)
  committedGenerationVector, committedDigest

CacheValue(slotDigest, generation, sourceRevision,
           producerRevision, payloadDigest, freshUntil, staleUntil)

任何会改变内容的更新,都在源事务中提高对应资源、集合或 projection namespace 的 requiredGeneration 并写 outbox;删除使用更高代 tombstone。复杂查询若无法声明有限 dependency namespace 就不可缓存。物理 DEL key 只是清理优化,不能承担正确性,因为消息会丢、旧值会从副本回来。数据库 CDC可以输送变更,但消费者仍要按 generation 条件应用。

读取时校验 slot 身份及 value 的每个 dependency generation。授权事务还要同步提高权威 auth epoch;每次返回前必须与 live epoch 比较,鉴权存储不可用就 fail closed,异步 outbox 只负责重建和清理。权限撤销、租户停用和删除一律不允许 stale;普通公共内容才可按版本化策略在 staleUntil 前返回旧值。RFC 5861 不会替应用判断哪些字段可陈旧。

Singleflight 负责合并,Fence 负责拒绝迟到者

进程内先按完整 slot digest 使用 singleflight,减少同实例重复工作。跨实例重建通过控制存储原子领取 (slot, requiredGeneration),得到 rebuildAttemptId、递增 fence 和短 lease;只有一个 attempt 为 leader,其他请求返回允许的 stale、等待有界时间或走限流旁路。lease 用来判断何时可接管,fence 才决定谁能提交。

leader 在明确 snapshot 读取 dependency generation 向量,生成 payload 后条件发布:所有 namespace generation、当前 fence、projection/policy version 和 auth epoch 都必须仍与领取时相同。先写不可变 value,再 CAS committed pointer;失败的迟到结果只能丢弃。租约续期同样比较 attempt 与 fence,旧 leader 不能在接管后续租或删除新 leader 的锁。乐观锁与悲观锁提供存储原语,这里把版本比较延伸到缓存发布接受点。

若 value 已写但 pointer 响应丢失,恢复器读取 pointer 和摘要确认,而不是创建新 generation。若源读取跨多个表,必须使用同一数据库快照或带版本的 projection;“查询开始时看到一半旧、一半新”即使 generation 正确也不是合格 value。回源失败不缓存成正常空值;只有 schema 明确区分的 NOT_FOUND 才能生成短期负缓存,权限错误、超时和 5xx 保持失败。

限制陈旧窗口和资源放大

每个 slot 策略声明 maxBuildTime、freshTtl、maxStale、wait budget、最大候选字节和 fallback。热点 miss 不能让无限请求都阻塞等待 leader;等待者达到上限后返回已有 stale 或明确 503。重建重试、数据库客户端重试和请求层重试共享同一总预算,防止一次击穿放大成多层风暴。混沌工程应验证的不只是命中率,还包括 source QPS 和陈旧接受边界。

故障套件让 A 在读完旧 snapshot 后暂停,推进 namespace generation 并由 B 发布,再恢复 A;还覆盖 query 依赖多条记录、CDC 乱序、权限事务已提交但 outbox 延迟、auth store 不可用、lease 接管、旧 key 复活和万人同时 miss。断言 committed generation 向量不倒退,撤权后旧值接受数为 0,正常 generation 只有一次源查询,旧 fence 更新全部失败。lease 到期后计算可能短暂重叠;fence 只承诺一个 commit,不能把重复计算伪装成已消除。

上线门要求竞态测试中陈旧覆盖为 0、无接管故障的热点 1 万并发 miss 对每个 slot/generation 只发起 1 次源查询、接管时重叠计算不超过 2 个且仅 1 个 commit、失效到新代可读的 P99 小于 5 秒、普通内容 stale 超窗接受为 0、权限类 stale 接受为 0,重建失败时缓存层内存和等待队列保持上限。先 shadow 计算 generation,再双写 envelope 不读,随后对公共低风险 slot 启用。回滚时关闭重建并旁路到源或只读已验证旧协议值;提高全局 cache epoch 让新 reader 拒绝问题代,不能重新启用无 fence 的旧 writer。

延伸阅读

Related

相关推荐