3、缓存一致性场景分析:读写缓存、缓存穿透、缓存击穿、缓存雪崩场景下的API契约需求
在分布式系统中,缓存是提升读取性能的核心手段,但缓存与数据库之间的数据不一致问题始终是治理难点。本章从四种经典缓存异常场景出发,分析每种场景对 API 契约提出的具体需求,为后续的错误处理与降级设计奠定场景基础。
3.1 读写缓存场景与一致性契约
读写缓存是最基础的缓存使用模式,其核心矛盾在于:写操作更新数据库后,缓存何时失效或同步。不同的更新策略对 API 的响应语义、幂等性、重试行为有截然不同的要求。
| 更新策略 | 一致性级别 | API 契约需求 | 典型适用场景 |
|---|---|---|---|
| Cache-Aside(旁路缓存) | 最终一致性 | 写 API 返回后异步删除缓存;读 API 需容忍短暂脏读 | 用户信息、商品详情 |
| Write-Through(穿透写) | 强一致性(同事务) | 写 API 必须返回缓存更新结果;失败时需回滚数据库 | 库存扣减、账户余额 |
| Write-Behind(异步写) | 弱一致性 | 写 API 仅确认缓存写入;数据库异步落盘需补偿接口 | 访问计数、日志聚合 |
契约设计要点: 在 OpenAPI 规范中,Cache-Aside 策略的写接口应在响应头中加入
X-Cache-Invalidation: true 标记,告知调用方缓存已失效;读接口应声明 Cache-Control: max-age=60 以明确容忍的过期窗口。
3.2 缓存穿透场景与契约需求
缓存穿透指请求查询一个数据库中也不存在的数据,导致每次请求都绕过缓存直达数据库。高并发下,穿透流量可能压垮后端存储。
3.2.1 穿透的典型成因
- 恶意攻击: 遍历不存在的 ID(如负数、超大值、随机字符串)。
- 业务漏洞: 前端未做参数校验,后端未做空值缓存。
- 缓存过期: 热点空值在缓存中过期后,再次被大量请求。
3.2.2 API 契约的防御性需求
| 契约维度 | 具体需求 | OpenAPI 示例 |
|---|---|---|
| 参数校验 | 对查询参数做格式、范围、枚举校验 | minimum: 1, maximum: 1000000 |
| 空值缓存 | 接口应返回标准空值响应,并声明缓存 TTL | 404 + X-Cache-Empty: true |
| 限流熔断 | 对高频无效查询实施速率限制 | 429 + Retry-After |
| 布隆过滤器 | 前置过滤接口,判断 key 是否存在 | POST /bloom/check |
常见误区: 仅依赖布隆过滤器而忽略 API 参数校验契约。布隆过滤器存在误判率,且无法覆盖动态生成的 key。正确的做法是:参数校验 → 布隆过滤 → 空值缓存,三层防御在契约中逐级声明。
3.3 缓存击穿场景与契约需求
缓存击穿指热点 key 在失效瞬间,大量并发请求同时穿透到数据库。与穿透不同,击穿的目标 key 在数据库中真实存在,但缓存恰好过期。
3.3.1 击穿的保护策略对比
| 策略 | 原理 | API 契约影响 | 一致性代价 |
|---|---|---|---|
| 互斥锁(Mutex) | 第一个请求加锁回源,其余等待 | 接口需声明超时时间与重试策略 | 低(串行回源) |
| 逻辑过期 | 缓存永不过期,后台异步更新 | 接口需返回过期标记,调用方自行决定是否等待 | 高(可能读到旧数据) |
| 分布式锁回源 | Redis Redlock 或 ZooKeeper 锁 | 接口需携带锁标识,支持锁释放回调 | 中(锁竞争开销) |
3.3.2 契约中的降级信号
当检测到击穿风险时,API 应通过响应头或状态码通知调用方进入降级模式:
X-Cache-Status: STALE— 返回的是过期缓存数据,调用方可接受降级。X-RateLimit-Remaining: 0— 触发限流,调用方应退避。503 + Retry-After: 5— 服务暂时不可用,建议 5 秒后重试。
最佳实践: 在 OpenAPI 的
responses 中为每个读接口显式定义 503 响应,并附带 headers.Retry-After 字段。这能让客户端 SDK 自动生成退避逻辑,无需人工干预。
3.4 缓存雪崩场景与契约需求
缓存雪崩指大量缓存 key 在同一时间过期,或缓存节点整体宕机,导致所有请求直接打到数据库。雪崩是穿透和击穿的叠加态,破坏力最大。
3.4.1 雪崩的 API 契约分层防御
- 过期时间离散化: 在缓存写入时,对基础 TTL 增加随机偏移(如 ±10%),避免批量过期。契约中应声明
X-Cache-TTL-Base和X-Cache-TTL-Jitter。 - 本地缓存兜底: 当远程缓存不可用时,API 应返回本地进程缓存(如 Caffeine)。契约需声明
X-Cache-Local: HIT或MISS。 - 数据库限流: 在 API 网关层对数据库查询路径实施令牌桶限流。契约中通过
429响应码和X-DB-RateLimit头传递限流状态。 - 熔断降级: 当数据库错误率超过阈值(如 50%),API 应直接返回降级响应。契约中定义
X-Circuit-Breaker: OPEN头。
雪崩场景下的契约核心原则: 任何读接口都必须声明 降级响应模型,包括降级数据的来源(本地缓存/默认值/空对象)、降级数据的时效性(stale-while-revalidate 窗口)、以及降级后的重试策略。这些信息应集中定义在 OpenAPI 的
x-cache-policy 扩展字段中。
3.5 四种场景的契约需求总览
下表汇总了四种缓存异常场景对 API 契约的差异化需求,供架构师在设计阶段逐项检查:
| 场景 | 核心风险 | 契约防御手段 | 降级信号 | 一致性要求 |
|---|---|---|---|---|
| 读写缓存 | 数据不一致 | 失效标记、事务回滚 | X-Cache-Invalidation | 最终/强(按策略) |
| 缓存穿透 | DB 被打穿 | 参数校验、空值缓存、布隆过滤 | 404 + X-Cache-Empty | 不要求(空值) |
| 缓存击穿 | 热点 key 失效 | 互斥锁、逻辑过期、分布式锁 | X-Cache-Status: STALE | 弱一致性可接受 |
| 缓存雪崩 | 批量过期/节点宕机 | TTL 离散化、本地缓存、限流熔断 | 503 + Retry-After | 最终一致性 |
3.6 场景驱动的契约设计流程
下图展示了从场景识别到契约落地的完整设计流程,帮助团队在 API 设计阶段系统性地覆盖缓存异常场景:
3.7 契约落地检查清单
在完成场景分析后,建议团队使用以下清单逐项确认 API 契约的完整性:
- ☐ 读接口是否声明了缓存命中状态?(X-Cache-Status: HIT/MISS/STALE)
- ☐ 写接口是否声明了缓存失效策略?(X-Cache-Invalidation: true + 失效 key 列表)
- ☐ 是否为空值查询定义了标准响应?(404 + X-Cache-Empty: true + 空值 TTL)
- ☐ 是否在 503 响应中声明了 Retry-After?(雪崩降级必备)
- ☐ 是否在 429 响应中声明了限流配额?(X-RateLimit-Remaining + X-RateLimit-Reset)
- ☐ 是否在扩展字段中定义了降级数据模型?(x-cache-policy.stale-while-revalidate)
- ☐ 是否对热点 key 的过期时间做了离散化声明?(X-Cache-TTL-Jitter)