加上缓存

我决定引入缓存来解决问题。

为什么选择缓存?

从上一节的分析我知道:

  • 同一个城市的天气,1 小时内基本不变
  • 但用户可能在 1 小时内请求多次
  • 重复调用外部 API 是巨大的浪费

缓存的工作原理

Weather API 有缓存的工作流程

用户请求北京天气

检查缓存

有北京的天气数据吗?
↓ 缓存命中

得到缓存数据

响应时间:20ms
↓ 缓存未命中

调用外部 API

响应时间:2000ms

存入缓存

过期时间:1 小时

存调用记录到 MySQL

用于统计分析

返回给用户

技术选型

我选择使用 Redis 作为缓存:

为什么是 Redis?

  • 速度快:内存存储,读写都在毫秒级
  • 简单易用:支持键值对存储
  • 支持过期时间:自动删除过期数据
  • 免费开源:有免费版本可用

选型边界

为什么此时引入 Redis

触发问题
天气数据变化慢,但同一城市会被反复查询;继续每次都调用外部 API,会让响应时间和供应商限流一起变差。
候选方案
进程内字典、文件缓存、MySQL 缓存表、Redis。
选择理由
Redis 支持 TTL、跨进程共享、读写延迟低,刚好匹配“短期缓存天气结果”的需求。
代价
系统多了一个外部依赖,后续必须处理缓存穿透、击穿、雪崩和缓存一致性问题。
暂不解决
暂不做 Redis 高可用和分片,因为当前数据量小,先验证缓存能否显著降低外部 API 调用量。

那时候的我还没想到,这个选择会在未来发挥更大的作用。

效果验证

上线后,我观察了一天的数据:

指标优化前优化后
平均响应时间2000ms35ms
缓存命中率-95%
外部 API 调用量10 万次/天5000 次/天

问题解决

  • 响应时间从 2 秒降到 35ms,用户不再抱怨
  • 外部 API 调用量降低 95%,不再担心限流
  • 成本几乎没增加(Redis 资源占用很小)

那一刻,我觉得所有的纠结和尝试都是值得的。

当前架构

技术架构
客户端

用户请求

GET https://dashboard.kuaiyizhi.cn/api/weather?city=北京
应用服务

Nginx

Python

Public API

Admin API

Static

Admin Dashboard


MySQL

Redis 缓存

外部服务

外部天气 API

练习

这五道题把缓存从「能用」推到「用得住」:命令怎么写、三种缓存问题怎么区分、数据一致性靠什么保证、Redis 能存什么,以及最后自己设计一条完整的缓存策略。

练习 1

Redis 中设置键值对过期时间的命令是?

这题考的是最基础的操作命令,但真正用到时还要清楚它与毫秒级、绝对时间两个变体的差别,才能在缓存时长需要精细控制时选对命令。

Redis过期时间基本命令

解题思路:

这是最基础的操作命令,但真正用到时还要分清秒级、毫秒级和绝对时间戳三个变体,才能在缓存时长需要精细控制时选对命令。

详细步骤:

最终答案:EXPIRE key seconds

扩展知识:

  • EXPIRE key seconds - 设置键的过期时间(秒)
  • PEXPIRE key milliseconds - 设置键的过期时间(毫秒)
  • EXPIREAT key timestamp - 设置键在指定时间戳过期
  • TTL key - 查看键的剩余生存时间(秒)

示例:

设计流程

练习 1:部署操作

  1. 步骤 1:在测试环境准备好 Redis 连接,并写入一个用来做实验的 key
  2. 步骤 2:执行 EXPIRE key seconds,给这个 key 设置剩余生存时间
  3. 步骤 3:用 TTL key 回读剩余秒数,确认过期时间已经生效
  4. 步骤 4:等时间到期后再次读取该 key,确认它已被 Redis 自动删除

关注点:过期命令的选择、TTL 回读验证与到期自动清理。

练习 2

缓存穿透、缓存击穿和缓存雪崩的区别是什么?

Redis缓存问题系统设计

解题思路:

三者最容易混着记,要按「成因 → 数据是否存在 → 并发特征」三个维度对照着看:穿透是查不存在的数据,击穿是热点 key 过期,雪崩是大量 key 同时失效或缓存服务宕机。

详细步骤:

三种缓存问题的区别:

问题原因解决方案
缓存穿透查询不存在的数据,请求直接打到外部 API布隆过滤器、缓存空对象
缓存击穿热点 key 过期,大量请求同时访问该 key互斥锁、逻辑过期
缓存雪崩大量 key 同时过期或 Redis 宕机随机过期时间、高可用架构

详细解释:

  1. 缓存穿透:用户查询的数据在缓存和外部 API 中都不存在,每次请求都会访问外部 API。

  2. 缓存击穿:某个热点 key 突然过期,此时大量请求同时访问这个 key,导致请求全部打到外部 API。

  3. 缓存雪崩:大量缓存 key 在同一时间过期,或者 Redis 服务器宕机,导致所有请求都涌向外部 API。

最终答案:

穿透——查询的数据压根不存在,请求直接打到外部 API,用布隆过滤器或缓存空对象防;击穿——热点 key 过期瞬间大量请求涌入,用互斥锁或逻辑过期防;雪崩——大量 key 同时过期或 Redis 宕机,用随机过期时间加高可用架构与降级方案防。

练习 3

如何保证缓存与外部 API 的数据一致性?

这一题没有唯一答案,关键是能说清三种方案各自的一致性代价:删除缓存比改写缓存更简单,主动更新最一致但最费事,而设置过期时间只是兜底。

Redis缓存一致性数据同步

解题思路:

这题没有唯一答案,关键是能说清三种方案各自的一致性代价:删除缓存比改写缓存更简单,主动更新最一致但最费事,而设置过期时间只是兜底。

详细步骤:

常见方案:

方案一:缓存失效策略

当外部 API 数据更新时,主动使缓存失效:

设计流程

练习 3

  1. 步骤 1:给每个缓存 key 设定合理的过期时间,作为失效失败时的兜底
  2. 步骤 2:外部 API 数据更新后,按城市或业务主键定位对应的缓存 key
  3. 步骤 3:删除该 key,让下一次读请求回源重建缓存,而不是就地改写缓存值
  4. 步骤 4:确认回源请求已经重建缓存,并观察紧随其后的读请求命中的是新值

关注点:失效触发时机、删除与改写的取舍以及回源重建。

方案二:设置缓存过期时间

  • 给缓存设置合理的过期时间
  • 过期后自动从外部 API 重新获取

方案三:主动更新缓存

设计流程

练习 3

  1. 步骤 1:订阅外部 API 的数据更新通知,或按固定周期扫描需要同步的城市
  2. 步骤 2:拉取最新数据并序列化成与缓存中完全一致的格式
  3. 步骤 3:用带过期时间的写入覆盖旧值,让缓存内容在写入瞬间就变成最新的
  4. 步骤 4:核对新旧值的差异和写入耗时,确认更新期间用户读到的仍是可用旧值

关注点:更新触发方式、写入格式一致性与更新期间的可用性。

各种方案对比:

方案优点缺点
缓存失效简单高效下次请求会变慢
设置过期时间自动恢复过期前数据可能不一致
主动更新一致性最好需要额外逻辑维护

最终答案:

三种方案按代价从低到高排列:① 缓存失效策略——外部 API 数据更新后主动删除缓存,配合合理的过期时间兜底,代价最低但存在短暂不一致窗口;② 主动更新缓存——一致性最好,但要在更新与缓存之间处理并发与失败;③ 只设过期时间——实现最简单,但一致性最弱。实践中通常以①为主、②用于关键数据、③作最后防线。

练习 4

Redis 有哪些常用的数据结构?分别适用于什么场景?

Redis数据结构应用场景

解题思路:

要按「能力 → 场景」两列来记:先说每种结构提供什么操作(键值、有序、字段、去重、带分数排序),再落到具体业务对象上。

详细步骤:

Redis 五大基本数据结构:

数据结构能力示例适用场景
String键值缓存缓存、计数器、分布式锁
List有序队列消息队列、最新列表
Hash对象字段存储对象、购物车
Set无序集合去重、好友关系、抽奖
ZSet带分数排序排行榜、带权重的排序

扩展数据结构:

  • Bitmap - 签到统计、用户在线状态
  • HyperLogLog - UV 统计
  • Geospatial - 附近的人、地理位置

最终答案:

String 用于键值缓存、计数器和分布式锁;List 用于消息队列与最新列表;Hash 用于存储对象和购物车;Set 用于去重、好友关系和抽奖;ZSet 用于排行榜与带权重的排序。

练习 5

设计一个 Redis 缓存策略,要求:

  • 用户信息查询先查缓存,缓存未命中再调用外部 API
  • 缓存过期时间设置为 30 分钟
  • 需要处理缓存未命中的情况

请用流程说明核心逻辑。

这道题要的是一个能落地的流程,而不是一句「加个缓存」。重点是 key 怎么拼、过期时间怎么设,以及未命中和查不到这两种情况分别怎么处理。

Redis缓存策略方案设计

解题思路:

这道题要的是一个能落地的流程,而不是一句「加个缓存」。重点是 key 怎么拼、过期时间怎么设,以及未命中和查不到这两种情况分别怎么处理。

详细步骤:

设计流程

练习 5

  1. 步骤 1:用 user:{id} 这样的命名规范拼出缓存 key,先查 Redis 判断是否命中
  2. 步骤 2:未命中时调用外部 API 取数,用带过期时间的写入方式缓存 30 分钟,并把回源结果返回给用户
  3. 步骤 3:对查询不到的用户写入带较短过期时间的空值标记,避免同一个不存在的 key 反复回源

关注点:key 命名规范、过期时间设置与空值的处理方式。

最终答案:

  1. 使用 user: 命名规范,便于管理
  2. 使用 SETEX 原子操作设置值和过期时间
  3. 空值也缓存,防止缓存穿透
  4. 合理设置过期时间,平衡性能和数据新鲜度

完整流程是:用 user:{id} 拼 key → 查 Redis → 命中直接返回;未命中则调用外部 API 取数,用 SETEX 原子写入并设置 30 分钟过期,同时把回源结果返回给用户;对查询不到的用户写入带较短过期时间的空值标记,避免同一个不存在的 key 反复回源。四个关键点:统一的 user: 命名规范便于管理、用 SETEX 原子设置值与过期时间、空值也要缓存以防穿透、过期时间在性能与数据新鲜度之间取平衡。

发现内容有问题?

告诉我们哪里需要改进。