加上缓存
我决定引入缓存来解决问题。
从上一节的分析我知道:
- 同一个城市的天气,1 小时内基本不变
- 但用户可能在 1 小时内请求多次
- 重复调用外部 API 是巨大的浪费
Weather API 有缓存的工作流程
用户请求北京天气
检查缓存
有北京的天气数据吗?得到缓存数据
响应时间:20ms调用外部 API
响应时间:2000ms存入缓存
过期时间:1 小时存调用记录到 MySQL
用于统计分析返回给用户
我选择使用 Redis 作为缓存:
为什么是 Redis?
- 速度快:内存存储,读写都在毫秒级
- 简单易用:支持键值对存储
- 支持过期时间:自动删除过期数据
- 免费开源:有免费版本可用
选型边界
为什么此时引入 Redis
- 触发问题
- 天气数据变化慢,但同一城市会被反复查询;继续每次都调用外部 API,会让响应时间和供应商限流一起变差。
- 候选方案
- 进程内字典、文件缓存、MySQL 缓存表、Redis。
- 选择理由
- Redis 支持 TTL、跨进程共享、读写延迟低,刚好匹配“短期缓存天气结果”的需求。
- 代价
- 系统多了一个外部依赖,后续必须处理缓存穿透、击穿、雪崩和缓存一致性问题。
- 暂不解决
- 暂不做 Redis 高可用和分片,因为当前数据量小,先验证缓存能否显著降低外部 API 调用量。
那时候的我还没想到,这个选择会在未来发挥更大的作用。
上线后,我观察了一天的数据:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 平均响应时间 | 2000ms | 35ms |
| 缓存命中率 | - | 95% |
| 外部 API 调用量 | 10 万次/天 | 5000 次/天 |
- 响应时间从 2 秒降到 35ms,用户不再抱怨
- 外部 API 调用量降低 95%,不再担心限流
- 成本几乎没增加(Redis 资源占用很小)
那一刻,我觉得所有的纠结和尝试都是值得的。
用户请求
GET https://dashboard.kuaiyizhi.cn/api/weather?city=北京Nginx
Public API
Admin API
Admin Dashboard
MySQL
Redis 缓存
外部天气 API
这五道题把缓存从「能用」推到「用得住」:命令怎么写、三种缓存问题怎么区分、数据一致性靠什么保证、Redis 能存什么,以及最后自己设计一条完整的缓存策略。
练习 1
Redis 中设置键值对过期时间的命令是?
这题考的是最基础的操作命令,但真正用到时还要清楚它与毫秒级、绝对时间两个变体的差别,才能在缓存时长需要精细控制时选对命令。
解题思路:
这是最基础的操作命令,但真正用到时还要分清秒级、毫秒级和绝对时间戳三个变体,才能在缓存时长需要精细控制时选对命令。
详细步骤:
最终答案:EXPIRE key seconds
扩展知识:
EXPIRE key seconds- 设置键的过期时间(秒)PEXPIRE key milliseconds- 设置键的过期时间(毫秒)EXPIREAT key timestamp- 设置键在指定时间戳过期TTL key- 查看键的剩余生存时间(秒)
示例:
练习 1:部署操作
- 步骤 1:在测试环境准备好 Redis 连接,并写入一个用来做实验的 key
- 步骤 2:执行 EXPIRE key seconds,给这个 key 设置剩余生存时间
- 步骤 3:用 TTL key 回读剩余秒数,确认过期时间已经生效
- 步骤 4:等时间到期后再次读取该 key,确认它已被 Redis 自动删除
关注点:过期命令的选择、TTL 回读验证与到期自动清理。
练习 2
缓存穿透、缓存击穿和缓存雪崩的区别是什么?
解题思路:
三者最容易混着记,要按「成因 → 数据是否存在 → 并发特征」三个维度对照着看:穿透是查不存在的数据,击穿是热点 key 过期,雪崩是大量 key 同时失效或缓存服务宕机。
详细步骤:
三种缓存问题的区别:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据,请求直接打到外部 API | 布隆过滤器、缓存空对象 |
| 缓存击穿 | 热点 key 过期,大量请求同时访问该 key | 互斥锁、逻辑过期 |
| 缓存雪崩 | 大量 key 同时过期或 Redis 宕机 | 随机过期时间、高可用架构 |
详细解释:
-
缓存穿透:用户查询的数据在缓存和外部 API 中都不存在,每次请求都会访问外部 API。
-
缓存击穿:某个热点 key 突然过期,此时大量请求同时访问这个 key,导致请求全部打到外部 API。
-
缓存雪崩:大量缓存 key 在同一时间过期,或者 Redis 服务器宕机,导致所有请求都涌向外部 API。
最终答案:
穿透——查询的数据压根不存在,请求直接打到外部 API,用布隆过滤器或缓存空对象防;击穿——热点 key 过期瞬间大量请求涌入,用互斥锁或逻辑过期防;雪崩——大量 key 同时过期或 Redis 宕机,用随机过期时间加高可用架构与降级方案防。
练习 3
如何保证缓存与外部 API 的数据一致性?
这一题没有唯一答案,关键是能说清三种方案各自的一致性代价:删除缓存比改写缓存更简单,主动更新最一致但最费事,而设置过期时间只是兜底。
解题思路:
这题没有唯一答案,关键是能说清三种方案各自的一致性代价:删除缓存比改写缓存更简单,主动更新最一致但最费事,而设置过期时间只是兜底。
详细步骤:
常见方案:
方案一:缓存失效策略
当外部 API 数据更新时,主动使缓存失效:
练习 3
- 步骤 1:给每个缓存 key 设定合理的过期时间,作为失效失败时的兜底
- 步骤 2:外部 API 数据更新后,按城市或业务主键定位对应的缓存 key
- 步骤 3:删除该 key,让下一次读请求回源重建缓存,而不是就地改写缓存值
- 步骤 4:确认回源请求已经重建缓存,并观察紧随其后的读请求命中的是新值
关注点:失效触发时机、删除与改写的取舍以及回源重建。
方案二:设置缓存过期时间
- 给缓存设置合理的过期时间
- 过期后自动从外部 API 重新获取
方案三:主动更新缓存
练习 3
- 步骤 1:订阅外部 API 的数据更新通知,或按固定周期扫描需要同步的城市
- 步骤 2:拉取最新数据并序列化成与缓存中完全一致的格式
- 步骤 3:用带过期时间的写入覆盖旧值,让缓存内容在写入瞬间就变成最新的
- 步骤 4:核对新旧值的差异和写入耗时,确认更新期间用户读到的仍是可用旧值
关注点:更新触发方式、写入格式一致性与更新期间的可用性。
各种方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 缓存失效 | 简单高效 | 下次请求会变慢 |
| 设置过期时间 | 自动恢复 | 过期前数据可能不一致 |
| 主动更新 | 一致性最好 | 需要额外逻辑维护 |
最终答案:
三种方案按代价从低到高排列:① 缓存失效策略——外部 API 数据更新后主动删除缓存,配合合理的过期时间兜底,代价最低但存在短暂不一致窗口;② 主动更新缓存——一致性最好,但要在更新与缓存之间处理并发与失败;③ 只设过期时间——实现最简单,但一致性最弱。实践中通常以①为主、②用于关键数据、③作最后防线。
练习 4
Redis 有哪些常用的数据结构?分别适用于什么场景?
解题思路:
要按「能力 → 场景」两列来记:先说每种结构提供什么操作(键值、有序、字段、去重、带分数排序),再落到具体业务对象上。
详细步骤:
Redis 五大基本数据结构:
| 数据结构 | 能力示例 | 适用场景 |
|---|---|---|
| String | 键值缓存 | 缓存、计数器、分布式锁 |
| List | 有序队列 | 消息队列、最新列表 |
| Hash | 对象字段 | 存储对象、购物车 |
| Set | 无序集合 | 去重、好友关系、抽奖 |
| ZSet | 带分数排序 | 排行榜、带权重的排序 |
扩展数据结构:
- Bitmap - 签到统计、用户在线状态
- HyperLogLog - UV 统计
- Geospatial - 附近的人、地理位置
最终答案:
String 用于键值缓存、计数器和分布式锁;List 用于消息队列与最新列表;Hash 用于存储对象和购物车;Set 用于去重、好友关系和抽奖;ZSet 用于排行榜与带权重的排序。
练习 5
设计一个 Redis 缓存策略,要求:
- 用户信息查询先查缓存,缓存未命中再调用外部 API
- 缓存过期时间设置为 30 分钟
- 需要处理缓存未命中的情况
请用流程说明核心逻辑。
这道题要的是一个能落地的流程,而不是一句「加个缓存」。重点是 key 怎么拼、过期时间怎么设,以及未命中和查不到这两种情况分别怎么处理。
解题思路:
这道题要的是一个能落地的流程,而不是一句「加个缓存」。重点是 key 怎么拼、过期时间怎么设,以及未命中和查不到这两种情况分别怎么处理。
详细步骤:
练习 5
- 步骤 1:用
user:{id}这样的命名规范拼出缓存 key,先查 Redis 判断是否命中 - 步骤 2:未命中时调用外部 API 取数,用带过期时间的写入方式缓存 30 分钟,并把回源结果返回给用户
- 步骤 3:对查询不到的用户写入带较短过期时间的空值标记,避免同一个不存在的 key 反复回源
关注点:key 命名规范、过期时间设置与空值的处理方式。
最终答案:
- 使用
user:命名规范,便于管理 - 使用
SETEX原子操作设置值和过期时间 - 空值也缓存,防止缓存穿透
- 合理设置过期时间,平衡性能和数据新鲜度
完整流程是:用 user:{id} 拼 key → 查 Redis → 命中直接返回;未命中则调用外部 API 取数,用 SETEX 原子写入并设置 30 分钟过期,同时把回源结果返回给用户;对查询不到的用户写入带较短过期时间的空值标记,避免同一个不存在的 key 反复回源。四个关键点:统一的 user: 命名规范便于管理、用 SETEX 原子设置值与过期时间、空值也要缓存以防穿透、过期时间在性能与数据新鲜度之间取平衡。
发现内容有问题?
告诉我们哪里需要改进。
