缓存策略

问题引入

上一节我给所有天气数据都设置了 1 小时的缓存时间。

上线运行一段时间后,我收到了用户的反馈:

用户 A:"为什么你们的数据总是比气象局慢?"
用户 B:"现在外面在下雨,你们 API 还显示晴天"
用户 C:"能不能实时更新天气数据?"

我意识到:1 小时的缓存时间太长了

但是,如果缩短缓存时间:

  • 外部 API 调用量会增加
  • 响应时间会变慢

如何平衡性能数据新鲜度?这成了我接下来要解决的问题。

数据分析

我研究了不同天气数据的变化规律:

当前温度

变化频率:实时变化
变化幅度:每分钟变化 0-1 度
用户期望:尽可能实时

天气状况

变化频率:几个小时变化一次
变化幅度:从晴天→阴天→雨天
用户期望:1 小时内更新即可

城市信息

变化频率:几乎不变
变化幅度:城市名、经纬度不变
用户期望:永久缓存

湿度数据

变化频率:几十分钟变化一次
变化幅度:变化 5-10%
用户期望:30 分钟内更新

分析完后,我明白了:不同数据的变化频率不同,不能用一个缓存时间对付所有情况。

优化策略

根据不同数据的变化频率,我设计了不同的缓存策略:

策略 1:分层缓存

设计流程
策略 1:分层缓存
  1. 步骤 1:按命中、未命中和异常 key 处理读取与回源
  2. 步骤 2:按本节策略读取、写入缓存并处理过期或失效
  3. 步骤 3:校验身份、密钥或权限
关注点:命中率、回源压力、TTL 和异常 key 处理。

缓存时间设置

数据类型缓存时间理由
实时温度5 分钟温度变化较快,用户期望实时
天气状况1 小时天气状况变化较慢
湿度数据30 分钟湿度变化中等速度
城市信息24 小时城市信息几乎不变
完整数据10 分钟平衡性能和新鲜度

策略 2:主动更新

有些关键数据,我不能等缓存过期才更新。

定时更新任务

设计流程
定时更新任务
  1. 步骤 1:写入缓存值、空值标记或热点保护状态
  2. 步骤 2:按本节策略读取、写入缓存并处理过期或失效
  3. 步骤 3:识别请求 key、缓存命中状态和回源数据对象
  4. 步骤 4:根据命中率、数据新鲜度和回源压力调整策略
关注点:命中率、回源压力、TTL 和异常 key 处理。

优势:

  • 用户访问时,缓存已经是新的
  • 热门城市数据始终保持新鲜

劣势:

  • 增加了外部 API 调用量
  • 需要维护热门城市列表

这个方案让我想起了”预加载”的概念——在用户需要之前,先把数据准备好。

策略 3:用户触发更新

允许用户主动刷新数据:

设计流程
策略 3:用户触发更新
  1. 步骤 1:按命中、未命中和异常 key 处理读取与回源
  2. 步骤 2:按本节策略读取、写入缓存并处理过期或失效
  3. 步骤 3:校验身份、密钥或权限
关注点:命中率、回源压力、TTL 和异常 key 处理。

使用方式:

# 普通请求(使用缓存)
GET /api/weather?city=北京

# 强制刷新(跳过缓存)
GET /api/weather?city=北京&refresh=true

这个方案把选择权交给了用户——对实时性要求高的场景,他们可以主动刷新。

最终方案

综合以上策略,我设计了这样的方案:

1. 分层缓存

  • 根据数据变化频率设置不同的缓存时间
  • 减少不必要的外部 API 调用

2. 定时更新

  • 对热门城市(Top 10)每 10 分钟主动更新
  • 保证高频访问城市的数据新鲜度

3. 用户可选

  • 提供 refresh 参数,让用户可以选择强制刷新
  • 满足对实时性要求高的场景

效果对比

优化前

指标数值
平均响应时间2000ms
外部 API 调用量10 万次/天
数据新鲜度最多延迟 1 小时

优化后(简单缓存)

指标数值
平均响应时间35ms
外部 API 调用量5000 次/天
数据新鲜度最多延迟 1 小时

优化后(分层缓存)

指标数值
平均响应时间40ms
外部 API 调用量8000 次/天
数据延迟:
- 温度数据最多 5 分钟
- 天气状况最多 1 小时
- 热门城市最多 10 分钟

权衡思考

分层缓存 vs 简单缓存:

维度简单缓存分层缓存
响应时间35ms40ms
API 调用量5000 次/天8000 次/天
数据新鲜度1 小时5-60 分钟
维护复杂度简单中等

我的结论: 对于天气数据,用户对数据新鲜度的要求高于响应时间,分层缓存是更好的选择

虽然维护复杂度增加了一些,但用户体验的提升是值得的。