🌍 译介 · 编译自 Codelit Engineering

API 缓存怎么存、怎么失效才不翻车

「计算机科学只有两件难事:缓存失效和命名。」缓存能极大提升 API 性能,但一旦失效策略没设计好,就会把过期数据喂给用户、把人逼疯。本文编译自 Codelit 工程系列《Cache Invalidation Strategies》(2026-03-29),系统梳理 9 种失效策略:TTL、事件驱动、Write-Through、Cache-Aside、标签、版本化键、Purge API、Stale-While-Revalidate、防缓存踩踏,并给出如何组合与选型。

📅 更新于 2026-07-24 ⏱ 约 14 分钟阅读 🏷 译介
免费试用 YesApi Pro 查看全部定价 →
🌐本文由 YesApi Pro 团队编译Codelit Engineering 的文章《Cache Invalidation Strategies: TTL, Event-Driven, Tags & More》,原作者 Codelit Team(原文发布于 2026-03-29)。版权归原作者所有,内容仅供学习参考。查看英文原文 →
📌 核心结论
缓存失效只有两条路:要么靠时间(TTL,允许短暂陈旧),要么靠事件(数据一变立刻清)。写时「删缓存」比「更新缓存」更安全;读多写少用 Cache-Aside,强一致用事件驱动或 Write-Through;CDN 用标签(Surrogate-Key)批量清;热门键务必防 Dogpile(锁/预热/概率提前刷新)。生产环境几乎都是多策略组合。
📑 本文目录
  1. 为什么失效策略至关重要
  2. 策略一:TTL(含自适应 TTL)
  3. 策略二:事件驱动失效
  4. 策略三:Write-Through / Write-Behind
  5. 策略四:Cache-Aside(懒加载)
  6. 策略五:标签失效(Surrogate-Key)
  7. 策略六:版本化键
  8. 策略七:Purge API
  9. 策略八:Stale-While-Revalidate
  10. 策略九:防缓存踩踏(Dogpile)
  11. 如何选型与组合
01

一、为什么失效策略至关重要

缓存的本质是「用空间换时间」:把算过/查过的结果暂存起来,下次直接用。但代价是——数据会过期。没有清晰失效策略的缓存,会 delivers 陈旧数据(stale data)、让用户困惑、把调试变成噩梦。

每条缓存条目都要回答一个问题:它什么时候不再正确? 这正是缓存设计里最难、也最容易被忽视的一环。下面 9 种策略,按「一致性强度 / 复杂度 / 适用场景」三维度帮你落地。

02

二、策略一:TTL(含自适应 TTL)

最简单粗暴:每条缓存值设置一个固定存活时长,到点自动过期。

SET user:42:profile "{...}" EX 300   # 5 分钟后过期

优点:简单、可预测、自带「自愈」——过期就重读。缺点:在 TTL 窗口内数据是陈旧的;TTL 太短浪费缓存、太长又一直喂旧数据。适用:读多、能容忍短暂陈旧的场景(商品列表、公开资料)。

进阶是自适应 TTL:数据越久没动,TTL 越长,减少无效回源:

def adaptive_ttl(last_modified, base_ttl=300):
    age = time.time() - last_modified
    if age > 86400:   # 24h 没改过
        return base_ttl * 4
    elif age > 3600:  # 1h 没改过
        return base_ttl * 2
    return base_ttl
03

三、策略二:事件驱动失效

数据一变就立刻清缓存——通过发布事件触发订阅方删除对应键。

用户更新资料
  -> publish "user:42:updated"
  -> 缓存订阅方删除 user:42:profile

优点:几乎零陈旧(near-zero staleness)。缺点:要引入事件基础设施(Kafka、Redis Pub/Sub、SNS),且写路径与缓存层产生耦合。适用:一致性要求高的数据(库存数量、账户余额)。即使用了事件驱动,也建议保留 TTL 作为兜底安全网。

04

四、策略三:Write-Through / Write-Behind

每次写操作同时更新缓存和数据库

def update_user(user_id, data):
    db.update("users", user_id, data)
    cache.set(f"user:{user_id}:profile", serialize(data), ex=3600)

优点:写完后缓存立刻是最新的。缺点:写延迟变高;若缓存写入失败就会产生不一致。变体 Write-Behind(Write-Back):先写缓存、异步刷库,写更快但有丢数据风险。

补充:纯缓存模式还有 Read-Through(缓存 miss 时由缓存自己去查库并回填)与 Write-Around(写直接落库、不进缓存,避免冷数据占缓存),可据读写比组合使用。

05

五、策略四:Cache-Aside(懒加载)

应用自己管缓存,最经典也最常用:

def get_user(user_id):
    cached = cache.get(f"user:{user_id}")
    if cached:
        return deserialize(cached)
    user = db.query("SELECT * FROM users WHERE id=%s", user_id)
    cache.set(f"user:{user_id}", serialize(user), ex=600)
    return user

def update_user(user_id, data):
    db.update("users", user_id, data)
    cache.delete(f"user:{user_id}")   # 失效,而不是更新

关键洞察:写的时候要删(delete)缓存条目,而不是更新它。下次读会从数据源重新加载,拿到的是真值。更新容易因并发竞态写出脏值,删除更稳。

06

六、策略五:标签失效(Surrogate-Key)

给缓存响应打标签,可以按组批量失效

Cache-Tag: product, product:42, category:electronics

商品更新时 purge 掉所有 product:42 标签;类目变动时 purge category:electronics。CDN 普遍支持:Fastly(Surrogate-Key)、Cloudflare(Cache-Tag)、Varnish(xkey)。

curl -X POST "https://api.fastly.com/service/{id}/purge/product:42" \
  -H "Fastly-Key: $TOKEN"

适用:聚合了多个实体的页面或响应。标签失效能大幅简化 CDN 缓存管理。

07

七、策略六:版本化键

把版本号直接塞进缓存键:

user:42:profile:v17

数据变化时版本号 +1,旧键靠 TTL 自然淘汰,无需显式删除

def get_user(user_id):
    version = db.query("SELECT cache_version FROM users WHERE id=%s", user_id)
    key = f"user:{user_id}:profile:v{version}"
    cached = cache.get(key)
    if cached:
        return deserialize(cached)
    # ... 取数并用该键缓存

优点:无竞态、无显式失效。缺点:旧版本在 TTL 到期前会占用内存。

08

八、策略七:Purge API

暴露一个显式端点,用来手动或流水线清特定缓存:

POST /api/admin/cache/purge
{ "patterns": ["user:42:*", "feed:home"] }

常用于人工干预CI/CD(部署触发 purge)。注意:Purge 端点必须鉴权 + 限流,否则会被滥用。

09

九、策略八:Stale-While-Revalidate

先立刻把旧值返回给用户,同时在后台异步拉取新副本:

Cache-Control: max-age=60, stale-while-revalidate=300

含义:60 秒内直接走缓存;之后 300 秒内仍先返回旧值,但后台重新校验。用户永远不用等缓存 miss代价:重校验期间会短暂返回陈旧数据。适合对首屏延迟敏感的用户态接口。

10

十、策略九:防缓存踩踏(Dogpile)

热门键过期的瞬间,成百上千请求会同时砸向数据库——这就是缓存踩踏(Dogpile / Cache Stampede)

解法一:加锁——只放一个请求去重算,其余等待或拿旧值:

def get_with_lock(key, fetch_fn, ttl=300):
    value = cache.get(key)
    if value:
        return value
    lock_key = f"lock:{key}"
    if cache.set(lock_key, "1", nx=True, ex=10):  # 抢到锁
        value = fetch_fn()
        cache.set(key, value, ex=ttl)
        cache.delete(lock_key)
        return value
    else:
        time.sleep(0.05)
        return cache.get(key)  # 稍后重试

解法二:概率提前过期——在 TTL 到期前随机提前刷新;解法三:预热——定时在过期前刷新热门键。三者可叠加。

11

十一、如何选型与组合

策略一致性复杂度适合
TTL最终一致通用缓存
事件驱动关键数据
Write-Through写多
Cache-Aside + 删最终一致读多
标签按需CDN / 聚合页
版本化键类不可变数据
Stale-While-Revalidate最终一致用户态延迟敏感

生产环境几乎不会只用一种,典型组合:CDN 层用「标签 + stale-while-revalidate」;应用缓存用「Cache-Aside + 事件驱动失效」;会话缓存用「TTL(短,15 分钟)」;配置缓存用「Write-Through + TTL(长,1 小时)」。

要点回顾:① 写时删缓存比更新更安全;② 即便用事件驱动,TTL 也得留作安全网;③ 热门键必须防 Dogpile;④ 标签失效极大简化 CDN 管理;⑤ Stale-While-Revalidate 让用户不等 miss 又不牺牲新鲜度。

译者实战注 落地建议与延伸

需要快速落地?

YesApi Pro 私有部署、源码交付、当天上线,帮您把方案变为现实。

立即预约演示 →

常见问题

它要回答「这条缓存什么时候不再正确」,涉及一致性、复杂度、性能的多方权衡,是计算机科学公认的两大难题之一(另一个是命名)。

应删除(delete)而非更新。删除让下次读从数据源重载真值,避免并发竞态写出脏值;更新容易在并发下产生不一致。

先立即返回旧缓存值,同时在后台异步重新校验。用户永不阻塞在缓存 miss 上,代价是重校验窗口内会短暂返回陈旧数据。

YesApi Pro 网关与后端可基于数据源变化触发事件失效,对外提供缓存策略配置与 CDN 友好响应头,把缓存失效策略落到平台基础设施层。
📚

继续阅读