代表性面试主题

后端面试:如何设计安全的 stale-while-revalidate 缓存合同?

后端困难
Offer.cc 编辑团队发布 更新

题干

一个读多写少的 API 需要在源站变慢时保持低延迟。你会如何使用 stale-while-revalidate 和 stale-if-error?哪些数据绝不能返回过期值?

题目与场景

你运营 GET /catalog,前面有 CDN 和应用缓存。部署期间源站可能变慢,但客户宁愿看到稍旧的目录,也不想看到空白页。请设计缓存响应头和重新验证路径,同时隔离租户数据并让新鲜度可度量。

假设目录按租户公开,写入经过源站,紧急价格变更必须很快可见。回答要区分可用性兜底和允许返回敏感状态的权限。

面试官考察什么

  • 是否理解新鲜、过期、重新验证,以及独立的 stale-if-error 策略。
  • 缓存键是否包含租户、授权、语言和内容协商边界。
  • 并发 miss 是否只触发一次源站请求,而不是惊群。
  • 运维能否撤销过期服务,并用指标证明新鲜度。

作答前的澄清问题

  1. 响应是公开、租户级还是用户级?私有数据可能根本不能放入 CDN 共享缓存。
  2. 正常读取和源站故障分别允许多旧?这会形成独立的新鲜窗口和过期窗口。
  3. 价格或权限变更能否立即使对象失效?如果能,应增加 purge 或版本化键,不能只依赖 TTL。
  4. 是否有验证器?ETagLast-Modified 可以把整包刷新变成条件重新验证。

30 秒回答框架

“我先定义新鲜窗口、有界的 stale-while-revalidate 窗口,以及独立的 stale-if-error 窗口。缓存键包含所有表示和授权边界,用户数据保持 private。过期命中快速返回,同时每个键只触发一次后台条件请求,并合并并发 miss。紧急变更通过 purge 或版本化键处理。指标覆盖年龄、重新验证结果、stale-if-error 使用和租户泄漏测试,运维可以关闭过期服务。”

分步骤深入解答

1. 分离新鲜度与可用性

max-age 定义响应保持新鲜的时间。stale-while-revalidate 允许缓存过期后在有界时间内先返回旧响应,同时后台重新验证。stale-if-error 是源站报错时独立的可用性允许。两者都不让旧数据自动正确;存在 must-revalidate 或适用的 no-cache 规则时不能随意复用。

公开目录可以使用短新鲜窗口和较长但有界的过期窗口。权限、余额或紧急价格应使用 privateno-store、purge 路径或更严格策略。窗口由业务风险决定,不由缓存默认值决定。

2. 构建安全缓存键与响应合同

缓存键必须包含租户、语言、编码,以及 Vary 指定的请求头。除非表示明确公开且与授权无关,否则不能让认证响应进入共享缓存。响应可以清晰声明策略:

http
Cache-Control: public, max-age=30, stale-while-revalidate=120, stale-if-error=600
Vary: Accept-Encoding, Accept-Language, X-Tenant-ID
ETag: "catalog-tenant-7-v42"

服务端必须确认 X-Tenant-ID 来自认证路由或 host,而不是任意客户端值。如果租户边界不能安全体现在缓存键中,就关闭共享缓存。

3. 在不惊群的情况下重新验证

过期命中时返回已有正文,并为每个缓存键排队一次重新验证。用短锁或 single-flight 映射,避免一万次读取产生一万次源站调用。重新验证发送 If-None-Match304 Not Modified 只刷新新鲜度,新的 200 则替换对象和验证器。

重新验证遇到临时错误时,只能在 stale-if-error 边界内继续提供旧对象,同时记录错误和年龄。不能每次失败都重置过期计时器,让窗口无限延长。

4. 让失效有明确路径

TTL 是安全网,不是紧急控制。价格或权限变更应发布版本化失效事件或清理受影响的键。写入路径可以先提交新版本,再发布事件;消费者需要幂等并支持重放。无法确认 purge 时,可以给受影响租户附加短 must-revalidate 期,或临时绕过该键缓存。

5. 度量并运营策略

跟踪响应年龄、新鲜命中率、stale-while-revalidate 命中率、stale-if-error 次数、重新验证延迟、304 比率、源站错误率、锁竞争和缓存键数量。对年龄接近最大值、stale-if-error 突增和跨租户测试失败告警。

提供功能开关或路由级 kill switch 停止返回过期内容。测试冷 miss、并发过期命中、源站超时、验证器变化、purge 竞态、租户头、语言变体和紧急价格更新。测试标准是新鲜度和隔离合同,不只是延迟降低。

高质量示范回答

我先给数据分类。公开的租户级目录可以设置 max-age=30stale-while-revalidate=120 和单独论证的 stale-if-error=600;用户或权限数据应保持 private 或不缓存。缓存键包含租户和表示维度,响应携带验证器。

过期命中时返回正文,并且每个键只做一次条件重新验证。304 刷新新鲜度,200 替换对象。临时源站故障可在有界 stale-if-error 时间内返回旧值,但不能无限重置计时器。写入发布幂等 purge 或版本事件处理紧急变更。我会监控年龄、过期使用、验证结果和租户隔离,并保留关闭过期服务的开关。

常见失误

  • 错误表现:对账户或权限数据使用 stale-while-revalidate失败原因:快速旧响应可能暴露无效授权判断 → 修正方法:敏感数据保持 private 或不缓存。
  • 错误表现:缓存键遗漏租户或语言 → 失败原因:一个表示可能跨边界提供 → 修正方法:推导并测试每个键维度。
  • 错误表现:每次失败重新验证都重置过期计时器 → 失败原因:故障数据可能永久存在 → 修正方法:执行绝对过期截止时间。
  • 错误表现:每个过期读取都请求源站 → 失败原因:过期流量会形成惊群 → 修正方法:按键合并重新验证。
  • 错误表现:把 TTL 当作紧急失效 → 失败原因:紧急变更要等到期 → 修正方法:发布 purge 或版本事件并确认完成。

追问与回答

为什么不只用 stale-if-error?

它只在源站请求发生错误时生效。stale-while-revalidate 用后台成功刷新改善正常延迟。两者解决的条件不同,需要独立边界和指标。

purge 期间验证器发生变化怎么办?

给对象版本化,并让 purge 事件幂等。看到旧验证器的重新验证不能覆盖更新版本;替换前比较对象版本或提交时间。顺序无法确认时,短时间绕过该键缓存。

CDN 可以缓存带 Vary: Authorization 的认证响应吗?

某些系统技术上可以,但风险很高。优先使用 private 或明确的租户公开表示。如果无法避免共享缓存,必须用集成测试和运维控制证明缓存键、授权独立性、purge 与跨租户隔离。

公开来源

同类题目