缓存策略

场景

新增多个 API 后,缓存策略需要优化。

问题分析:

  • 新闻数据:10 分钟更新一次
  • 股票数据:实时变化,需要 5 秒缓存
  • IP 数据:几乎不变,可以缓存 24 小时
  • 天气数据:每小时变化明显

当前问题:

  • 所有 API 都使用相同的缓存时间
  • 有些数据更新太慢
  • 有些数据缓存时间浪费

数据特征分析

按更新频率分类

实时数据(秒级):

  • 股票价格
  • 汇率

准实时数据(分钟级):

  • 新闻列表
  • 天气温度

静态数据(小时级):

  • IP 归属地
  • 城市信息

不变数据(永久):

  • 国家列表
  • 货币代码

分层缓存策略

L1: 应用内存缓存(最快)

L1 缓存跟着应用进程走,读一次就是一次内存查找,没有网络开销,是整条缓存链路里最快的一层。但它天生是每台机器各存一份,所以只能当加速层,不能当唯一数据源。

设计流程

L1: 应用内存缓存(最快)

  1. 步骤 1:在应用进程里用带 TTL 的内存字典存放序列化后的响应
  2. 步骤 2:设置容量上限与淘汰策略,优先淘汰即将过期的条目
  3. 步骤 3:命中时直接返回,未命中则回源并把结果写回内存缓存
  4. 步骤 4:多副本部署时明确它只做单机加速,不承担跨实例的一致性

关注点:进程内缓存的淘汰策略,以及多副本部署下的一致性。

L2: Redis 缓存(共享)

L2 是所有应用实例共享的一层 Redis,它解决了「每台机器各存一份」带来的重复回源问题,也是真正决定整体回源压力的地方。TTL 在这一层按数据类型分开设置,是整份缓存策略的核心。

设计流程

L2: Redis 缓存(共享)

  1. 步骤 1:用接口名与请求参数拼出缓存 key,统一序列化格式后写入 Redis
  2. 步骤 2:写入时设置 TTL,并按数据类型给不同接口配上不同的过期时间
  3. 步骤 3:命中直接返回,未命中时回源并回填 Redis,同时回填 L1 内存缓存
  4. 步骤 4:对访问量极高的 key 单独统计,避免集中过期或单点压力

关注点:共享缓存的序列化方式、过期策略与热点 key 压力。

智能缓存策略

策略 1: 热点数据预热

热点数据的第一个请求总是最慢,因为缓存里根本没有它。预热就是让这个代价提前付掉——用定时任务在低峰期把热点名单先跑一遍,用户上线后就不用再等第一次回源。

设计流程

策略 1: 热点数据预热

  1. 步骤 1:从访问日志里统计近期访问量最高的 key,形成热点名单
  2. 步骤 2:用定时任务在低峰期按名单提前回源并写入缓存,给热点 key 设更长的 TTL
  3. 步骤 3:观察预热后首访延迟是否下降,确认热点名单仍然准确

关注点:热点 key 的识别方式,以及预热流量与真实流量的冲突。

策略 2: 缓存穿透保护

穿透的本质是请求了根本不存在的 key,缓存永远不命中,数据库被反复打穿。最省事的办法是缓存「空」这个事实本身——但空值的 TTL 必须短,否则新数据就再也上不来了。

设计流程

策略 2: 缓存穿透保护

  1. 步骤 1:对查询结果为空的 key 也写入短 TTL 的空值标记
  2. 步骤 2:请求命中空值标记时直接返回空结果,不再回源查询
  3. 步骤 3:统计空值命中的比例,异常升高时收紧参数并排查扫描型请求

关注点:不存在的 key 被反复请求时,如何把回源挡在缓存层之外。

策略 3: 缓存雪崩防护

雪崩是很多 key 在同一时刻集体过期,回源请求在同一秒涌向数据源。防护思路有两条:把过期时间打散,把同一 key 的并发回源合并。两条一起做才稳。

设计流程

策略 3: 缓存雪崩防护

  1. 步骤 1:给 TTL 叠加随机抖动,并把过期时间按数据类型分成几档,避免大批 key 同时失效
  2. 步骤 2:回源时加分布式锁或单飞保护,同一 key 只放一个请求去查询
  3. 步骤 3:观察过期瞬间的回源曲线,确认尖峰已经被削平

关注点:大量 key 同时过期时,如何避免回源压力集中爆发。

按数据类型缓存

股票数据(短期缓存)

股票是典型的秒级数据:慢一点用户就有意见,但真做到毫秒级实时,成本又完全不成比例。5 秒 TTL 是我在这两者之间取的折中。

设计流程

股票数据(短期缓存)

  1. 步骤 1:按行情接口设定 5 秒左右的 TTL,容忍一两个 tick 的延迟
  2. 步骤 2:缓存 value 里同时存最新价与更新时间戳,回源失败时返回上一次的价格并标记为陈旧
  3. 步骤 3:在高频时段观察命中率与延迟,确认 TTL 取值合理

关注点:秒级行情对新鲜度的要求,以及缓存与实时推送的边界。

新闻数据(中期缓存)

新闻比行情慢,但比天气快,10 分钟的 TTL 比较贴合它的更新节奏。列表接口还有个坑:分页和筛选条件会组合出大量 key,缓存粒度设计不好,命中率会被稀释。

设计流程

新闻数据(中期缓存)

  1. 步骤 1:把列表接口按页码和筛选条件拆成独立的缓存 key
  2. 步骤 2:给新闻列表设 10 分钟左右的 TTL,抓到新批次后主动失效对应缓存
  3. 步骤 3:观察列表类接口的命中率,确认没有大量分页 key 重复回源

关注点:列表类接口的分页缓存粒度,以及更新之后的失效时机。

IP 数据(长期缓存)

IP 归属地几乎不变,是最适合长缓存的一类数据:24 小时 TTL 换来的命中率提升非常可观。唯一要注意的是 IP 库更新时得主动失效,否则新分配的 IP 会查不到归属地。

设计流程

IP 数据(长期缓存)

  1. 步骤 1:给 IP 归属地接口设 24 小时 TTL,与数据的几乎不变特性对齐
  2. 步骤 2:缓存命中后直接返回,IP 库版本更新时按版本号批量失效对应的缓存
  3. 步骤 3:统计长 TTL 接口的命中率,验证它们确实是流量的大头

关注点:几乎不变的数据如何避免重复回源,以及 IP 库更新的可见性。

缓存监控

缓存命中率统计

命中率是衡量缓存策略的第一指标,但整体平均值会骗人:一个高频接口就能把数字拉得很漂亮。所以统计一定要分接口做,并且把回源次数和耗时一起看。

设计流程

缓存命中率统计

  1. 步骤 1:在缓存读写两处埋点,分别累加命中次数与请求总次数
  2. 步骤 2:按接口维度分别计算命中率,避免高频接口掩盖低命中接口
  3. 步骤 3:同时统计回源耗时与回源次数,命中率之外还要看回源成本
  4. 步骤 4:定期对比不同 TTL 下的命中率,为调整过期时间提供依据

关注点:命中率怎么算才不骗人,以及分接口统计的必要性。

发现内容有问题?

告诉我们哪里需要改进。