13 · 缓存与 Redis
缓存是"用一致性换性能"。前端有 HTTP 缓存,后端有 Redis。本篇讲清缓存值不值得加、怎么加不出事、Redis 常用什么。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | Redis 是什么、缓存什么时候该加、三大缓存问题怎么防 |
| 核心内容 | 缓存的价值与代价、Redis 常用结构、缓存穿透/击穿/雪崩、一致性策略、在 FastAPI 接入 |
| 读完能回答 | 我该不该加缓存?缓存和数据库不一致怎么办?高并发下缓存为什么会崩? |
| 相关板块 | C10 · 数据建模 | C04 · 一致性事务 | C07 · 性能与容量 |
一、先回答:该不该加缓存
| 场景 | 加缓存 | 理由 |
|---|---|---|
| 热点数据高频读(配置、榜单、用户资料) | ✅ | 命中率高,DB 压力骤降 |
| 复杂计算/聚合结果 | ✅ | 算一次存 N 次 |
| 只有低频读 | ❌ | 缓存本身有维护成本 |
| 写多读少 | ❌ | 缓存收益低,还容易不一致 |
| 数据一致性要求极高(账务) | ❌ 慎用 | 缓存引入延迟不一致 |
判断顺序:先确认"读频率真的高"→ 再想"不一致的窗口可不可接受"→ 都成立才加。
二、Redis 常用结构(使用侧)
| 结构 | 用途 | 类比前端 |
|---|---|---|
String | 缓存值、计数器、限流计数 | 一个 key-value |
Hash | 对象字段读写 | 一个对象(可只改某个字段) |
List | 消息队列、最近列表 | 数组(左进右出) |
Set | 去重、集合运算 | Set |
ZSet | 排行榜、延迟队列 | 有序集合(score 排序) |
python
import redis
r = redis.Redis.from_url("redis://localhost:6379/0")
r.set("user:42", '{"name": "张三"}', ex=3600) # ex=过期秒数
value = r.get("user:42")
ex过期是缓存必备:没有过期时间的缓存 = 永不失效的脏数据。
三、三大缓存问题
3.1 穿透(查不存在的数据)
现象:请求的数据既不在缓存也不在库,每次都打 DB。
解法:
- 布隆过滤器(前置拦截)
- 缓存空值:不存在的 key 也缓存一个短 TTL 的空值
python
def get_user(uid):
data = r.get(f"user:{uid}")
if data is not None:
return data
row = db.get_user(uid)
if row is None:
r.set(f"user:{uid}", "", ex=60) # 缓存空值,60s
return None
r.set(f"user:{uid}", row.to_json(), ex=3600)
return row3.2 击穿(热点 key 过期瞬间)
现象:一个热点 key 过期,大量请求同时打 DB。
解法:
- 互斥锁:重建缓存时只让一个请求去查库
- 逻辑过期 + 后台刷新(热点数据不真删)
3.3 雪崩(大量 key 同时过期)
现象:大量 key 同一时刻过期,DB 被洪峰打挂。
解法:
- 过期时间加随机抖动(
ex + random(0, 300)) - 多级缓存(本地缓存兜底)
一句话记忆:穿透是"查了不存在的",击穿是"一个热点挂了",雪崩是"一群一起挂"。分别用空值缓存、互斥锁、随机过期防。
四、缓存一致性:删 or 更新
| 策略 | 做法 | 问题 |
|---|---|---|
| 先更新 DB,再删缓存 | 主流 | 删除失败会短暂脏读(配合 TTL 兜底) |
| 先更新 DB,再写缓存 | 简单 | 并发下可能写回旧值 |
| 先删缓存,再更新 DB | ❌ 别用 | 中间读会重建旧缓存 |
python
def update_user(uid, data):
db.update_user(uid, data)
r.delete(f"user:{uid}") # 更新后删缓存,让下次读重建推荐默认:先更新 DB,再删缓存,依赖 TTL 兜底。追求强一致就别用缓存(见 C04)。
五、限流与分布式锁(同库附带)
Redis 常顺带承担两个任务:
python
# 限流:固定窗口
def allow(uid, limit=100, window=60):
key = f"rate:{uid}"
count = r.incr(key)
if count == 1:
r.expire(key, window)
return count <= limitpython
# 分布式锁:注意设置过期,防止死锁
ok = r.set("lock:order:42", "1", nx=True, ex=10)分布式锁只解决"跨实例的互斥",锁必须有过期时间,否则持有方挂了会死锁。
自检清单
- [ ] 能用"读频率 × 一致性要求"判断该不该加缓存
- [ ] 能说清穿透/击穿/雪崩的区别与各自解法
- [ ] 记得写缓存必带过期时间(
ex) - [ ] 会"先更新 DB 再删缓存"的一致性写法
- [ ] 会用 Redis 做简单限流和分布式锁,并给锁设过期
常见误区
- 误区:缓存越大越好。 命中率低的高频写缓存是负收益。
- 误区:缓存永远一致。 缓存本质是"允许短暂不一致",要求强一致就别加。
- 误区:分布式锁不设过期。 不设过期 = 进程一挂全系统锁死。
- 误区:三个缓存问题分不清。 穿透查不存在、击穿热点单个、雪崩集体过期。
动手练习
练习 A:热点缓存
给用户详情接口加 Redis 缓存(含过期),并发压测对比 DB 直读与带缓存的 QPS。
练习 B:防空值
给一个"查不存在的 id"的接口做空值缓存,验证穿透被拦截。
练习 C:限流
给接口加固定窗口限流,超限返回 429。
相关文档
本区
- 10 · 数据建模与数据访问 —— 缓存服务于数据层
- 04 · 一致性与事务 —— 缓存一致性 vs 强一致
- 07 · 性能与容量 —— 性能优化的完整方法论
- 21 · AI 服务可靠性 —— 语义缓存在 AI 场景
其他域
- DB 数据库与存储 —— Redis 之外的存储选型
- 10 · Web 性能优化 —— 缓存思维的前端对照