Redis 缓存问题
Redis 常被用作缓存,用来减轻数据库压力、提升接口响应速度。但缓存不是简单地“查 Redis,没有再查数据库”。如果设计不好,会出现缓存穿透、击穿、雪崩、热 Key、大 Key 等问题。
学习目标
学完这一页,你要能做到:
- 分清缓存穿透、击穿、雪崩、大 Key、热 Key 的本质区别。
- 画出 Cache Aside 的正常读流程,以及异常流量如何打穿数据库。
- 解释空值缓存、布隆过滤器、互斥锁、逻辑过期、TTL 随机分别解决什么问题。
- 说清楚为什么这些方案都不是银弹,以及不这样做会出现什么线上事故。
- 能为资产详情、商品详情、字典配置、订单查询这类商业场景选择合适方案。
- 能写出基础 Demo,并知道真实项目还要加重试、限流、监控和降级。
缓存查询基本流程
flowchart TD
A["用户请求"] --> B["查询 Redis"]
B --> C{"缓存是否命中"}
C -- "命中" --> D["返回缓存数据"]
C -- "未命中" --> E["查询数据库"]
E --> F{"数据库是否有数据"}
F -- "有" --> G["写入 Redis"]
F -- "无" --> H["缓存空值或直接返回"]
G --> I["返回数据"]
H --> I这个流程看起来简单,但高并发下会放大很多问题。
问题对比
| 问题 | 含义 | 典型原因 | 常见处理 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据,每次都打到数据库 | 恶意请求或无效 ID | 缓存空值、布隆过滤器 |
| 缓存击穿 | 热点 Key 过期,大量请求同时回源 | 热点数据失效 | 互斥锁、逻辑过期 |
| 缓存雪崩 | 大量 Key 同时失效 | 统一过期时间、Redis 故障 | 过期随机、预热、降级 |
| 热 Key | 某个 Key 访问极高 | 热点文章、秒杀商品 | 本地缓存、拆分、限流 |
| 大 Key | 单个 Key 数据过大 | 大集合、大 JSON | 拆分、限制大小 |
本页先讲缓存问题总览。大 Key、热 Key、慢命令、连接池耗尽、Cluster 热点和生产排查的完整治理方案,继续看:Redis 高并发缓存治理。
这些问题的本质区别
很多人背题时会把穿透、击穿、雪崩混在一起。可以先按“压力为什么绕过缓存”来理解:
flowchart TD
A["缓存保护数据库失败"] --> B{"失败原因"}
B -- "查的数据根本不存在" --> C["缓存穿透"]
B -- "一个热点 Key 刚好失效" --> D["缓存击穿"]
B -- "大量 Key 同时失效或 Redis 故障" --> E["缓存雪崩"]
B -- "单个 Key 太大" --> F["大 Key"]
B -- "单个 Key 访问太高" --> G["热 Key"]| 问题 | 核心矛盾 | 压力主要打到哪里 | 一句话处理 |
|---|---|---|---|
| 穿透 | 无效请求绕过缓存 | 数据库 | 先证明这个 ID 是否可能存在 |
| 击穿 | 热点 Key 失效后并发回源 | 数据库 | 同一时刻只允许少量线程重建 |
| 雪崩 | 大面积缓存同时不可用 | 数据库和应用 | 错开过期、限流降级、高可用 |
| 大 Key | 单次命令太重 | Redis 主线程、网络、客户端 | 拆小、分页、限制长度 |
| 热 Key | 单 Key 流量集中 | Redis 单节点或单 slot | 本地缓存、拆热点、限流 |
判断事故时不要只背定义。数据库 QPS 飙升,优先想穿透、击穿、雪崩;Redis 单节点 CPU 或网络高,优先想热 Key;慢日志出现 HGETALL、SMEMBERS、LRANGE 0 -1,优先想大 Key。
缓存穿透
缓存穿透指查询一个数据库里也不存在的数据。因为缓存没有,数据库也没有,所以每次请求都会打到数据库。
示例:
请求 articleId = -1
Redis 没有
MySQL 也没有
下次请求仍然重复打 MySQL方案一:缓存空值
数据库查不到时,也在 Redis 中缓存一个空结果,设置较短过期时间。
article:-1 -> null,过期时间 1 分钟优点:简单。
缺点:如果无效 Key 非常多,会占用 Redis 空间。
方案二:布隆过滤器
布隆过滤器可以快速判断一个 ID 是否“可能存在”。
flowchart TD
A["请求 ID"] --> B["布隆过滤器判断"]
B --> C{"可能存在吗"}
C -- "否" --> D["直接拒绝或返回空"]
C -- "是" --> E["继续查缓存和数据库"]布隆过滤器可能误判存在,但不会把一定不存在的数据放过去太多。它的底层是“位数组 + 多个哈希函数”:添加元素时把多个 bit 置为 1,查询时只要有一个 bit 为 0,就说明一定不存在;如果所有 bit 都是 1,只能说明可能存在。完整原理、误判原因、参数估算和 Demo 看:布隆过滤器原理。
穿透治理怎么选
| 请求类型 | 推荐方案 | 原因 |
|---|---|---|
| 明显非法参数 | 参数校验直接拒绝 | 成本最低,不要把垃圾流量放进缓存层 |
| 重复查询同一个不存在 ID | 空值缓存 | 简单有效,短 TTL 即可 |
| 大量随机不存在 ID | 布隆过滤器 | 空值缓存会产生海量无效 key,布隆过滤器更省内存 |
| 恶意攻击流量 | 限流 + 黑名单 + 布隆过滤器 | 只靠缓存方案不够 |
| 新增数据频繁 | 布隆过滤器要可靠增量更新 | 否则新数据可能被误拦 |
商业系统通常组合使用:
flowchart TD
A["查询资产详情"] --> B["参数格式和范围校验"]
B --> C["布隆过滤器判断 assetId"]
C --> D{"一定不存在"}
D -- "是" --> E["直接返回空"]
D -- "否" --> F["查 Redis"]
F --> G{"命中"}
G -- "是" --> H["返回"]
G -- "否" --> I["查 MySQL"]
I --> J{"MySQL 是否存在"}
J -- "存在" --> K["写正常缓存"]
J -- "不存在" --> L["写短 TTL 空值"]不这样做会怎样:
- 攻击者随机构造 ID,Redis 永远不命中。
- 每个请求都查 MySQL。
- MySQL 连接池被无效查询占满。
- 正常用户请求也变慢。
- 应用线程堆积,接口超时。
缓存击穿
缓存击穿指一个热点 Key 过期,大量请求同时发现缓存没有,于是一起打数据库。
例如热门商品、数据资产详情、字典配置、采集任务状态这类热点缓存过期,瞬间上千请求同时查数据库。
flowchart TD
A["大量请求访问热点 Key"] --> B{"缓存是否过期"}
B -- "是" --> C["竞争重建锁"]
C --> D{"是否拿到锁"}
D -- "是" --> E["查询数据库并写缓存"]
D -- "否" --> F["等待、重试或返回旧值"]方案一:互斥锁
只有拿到锁的线程去查数据库并重建缓存,其他线程等待或稍后重试。
优点:避免数据库被打爆。
缺点:实现复杂一些,等待会增加延迟。
方案二:逻辑过期
缓存数据本身不过期,而是在值里保存逻辑过期时间。
{
"data": "...",
"expireAt": "2026-06-27 10:00:00"
}如果逻辑过期,先返回旧值,再异步重建缓存。
适合读多写少、允许短时间旧数据的场景。
互斥锁和逻辑过期怎么选
| 方案 | 优点 | 缺点 | 适合 |
|---|---|---|---|
| 互斥锁 | 数据更接近实时,只让一个线程回源 | 等待线程延迟增加,锁要防误删和超时 | 详情页、普通热点缓存 |
| 逻辑过期 | 用户请求不等待数据库,抗峰值好 | 会短暂返回旧数据 | 商品详情、文章详情、字典配置 |
| 永不过期 + 主动刷新 | 热点稳定,延迟低 | 主动刷新失败会旧值长期存在 | 首页配置、榜单快照 |
| 直接查库 | 实现简单 | 热点过期会打爆数据库 | 低并发或非热点 |
互斥锁流程:
flowchart TD
A["热点 Key 未命中"] --> B["尝试 set nx 获取重建锁"]
B --> C{"是否拿到锁"}
C -- "是" --> D["查 MySQL"]
D --> E["写 Redis"]
E --> F["释放锁"]
C -- "否" --> G["短暂 sleep 或返回旧值"]
G --> H["再次查 Redis"]逻辑过期流程:
flowchart TD
A["读取 Redis 数据"] --> B{"逻辑过期了吗"}
B -- "否" --> C["直接返回"]
B -- "是" --> D["尝试获取重建锁"]
D --> E{"是否拿到锁"}
E -- "是" --> F["异步重建缓存"]
E -- "否" --> G["直接返回旧值"]
F --> H["更新数据和逻辑过期时间"]注意:逻辑过期不是强一致方案。账户余额、支付状态、库存最终扣减、权限最终判断,不应该为了抗击穿就随便返回旧值。
缓存雪崩
缓存雪崩指大量 Key 同时失效,或者 Redis 整体不可用,导致大量请求同时打到数据库。
常见原因:
- 缓存过期时间都设置成 30 分钟。
- 批量预热时同一时间写入。
- Redis 集群故障。
解决方式:
- 过期时间加随机值。
- 热点数据提前预热。
- Redis 高可用。
- 接口限流和降级。
- 多级缓存。
示例:
int ttl = 3600 + RandomUtils.nextInt(0, 300);雪崩为什么比击穿更危险
击穿通常是一个热点 Key 过期,雪崩是大量 Key 或整个 Redis 层不可用。
flowchart TD
A["大量 Key 同时过期"] --> B["大量请求同时未命中"]
B --> C["数据库连接池被打满"]
C --> D["SQL 变慢"]
D --> E["应用线程等待"]
E --> F["接口超时扩大"]雪崩治理要分两类:
| 类型 | 原因 | 治理 |
|---|---|---|
| 过期雪崩 | TTL 设置太集中 | TTL 随机、分批预热、热点不过物理过期 |
| 故障雪崩 | Redis 节点或网络异常 | 高可用、熔断、限流、降级、本地兜底 |
Redis 异常时,不能让所有请求无脑查数据库。成熟系统要限制回源并发:
flowchart TD
A["Redis 异常"] --> B["缓存访问快速失败"]
B --> C["判断是否核心接口"]
C -- "非核心" --> D["降级返回兜底"]
C -- "核心" --> E["限量回源数据库"]
E --> F["保护 DB 连接池"]否则 Redis 只是“坏了一个组件”,但数据库被打爆后会变成“整站不可用”。
热 Key
热 Key 指某个 Key 被极高频访问。
危害:
- 单个 Redis 节点压力过大。
- 网络流量集中。
- 请求延迟升高。
处理方式:
- 本地缓存一份短时间数据。
- 拆分 Key。
- 热点数据预热。
- 对热点接口限流。
- 监控 Key 访问频率。
热 Key 的完整识别、Cluster 热点、本地缓存、逻辑过期和限流方案看:大 Key 与热 Key 全过程治理。
热 Key 和缓存击穿经常同时出现:
- 某个商品详情是热 Key。
- Redis 能扛住正常读。
- 这个 Key 到期或被删除。
- 所有请求同时回源数据库。
- 热 Key 变成击穿事故。
所以热点数据要么提前预热,要么逻辑过期,要么用互斥锁重建。不能只设置普通 TTL 就不管了。
大 Key
大 Key 指单个 Key 的 value 很大,例如:
- 一个 String 存 5MB JSON。
- 一个 List 有几十万元素。
- 一个 Hash 有大量字段。
危害:
- 网络传输慢。
- 删除阻塞。
- Redis 内存不均衡。
- 操作耗时长。
处理方式:
- 拆分成多个 Key。
- 限制集合大小。
- 使用分页。
- 删除时使用异步删除。
- 定期扫描大 Key。
大 Key 的阈值判断、扫描命令、拆分模型、异步删除和生产风险看:大 Key 与热 Key 全过程治理。
大 Key 和缓存雪崩也可能有关:大量大 Key 同时过期或被删除时,Redis 清理、AOF rewrite、主从复制和网络都会抖动。所以大 Key 治理不只是“节省内存”,也是稳定性治理。
生产建议
- 缓存空值设置短 TTL。
- 热点 Key 不要使用统一过期时间。
- 缓存重建要防并发击穿。
- Redis 异常时要限流保护数据库。
- 监控命中率、慢命令、内存、热 Key、大 Key。
- 缓存数据要能容忍短时间不一致。
商业场景选型
| 场景 | 主要风险 | 推荐组合 |
|---|---|---|
| 资产详情 | 穿透、击穿、旧数据 | 参数校验 + 布隆过滤器 + Cache Aside + 写库后删缓存 + TTL |
| 商品详情 | 热 Key、击穿、雪崩 | 预热 + 本地缓存 + 逻辑过期 + 限流 |
| 字典配置 | 热 Key、本地缓存一致性 | Redis + Caffeine + 短 TTL + 变更广播 |
| 订单查询 | 读己之写、强一致 | 写后读库或返回最新值,缓存只做展示加速 |
| 搜索筛选 | 缓存组合爆炸 | ES 或搜索服务,不要无限拼 Redis key |
| 统计看板 | 大范围聚合 | 汇总表或离线任务,Redis 存结果快照 |
| 不存在 ID 查询 | 穿透 | 参数校验 + 空值缓存 + 布隆过滤器 |
选型时先问三个问题:
- 这个数据能不能短暂旧?
- 这个 Key 会不会变大或变热?
- Redis 异常时,能不能查数据库,查多少,查不到返回什么?
商业 Demo:Cache Aside 查询资产详情
Cache Aside 是商业项目最常用的缓存模式:应用先查缓存,缓存没有再查数据库,然后把结果写回缓存。
public AssetDTO getAsset(Long assetId) {
String key = "asset:detail:" + assetId;
AssetDTO cached = redis.get(key, AssetDTO.class);
if (cached != null) {
return cached;
}
AssetDTO asset = assetRepository.findDetail(assetId);
if (asset == null) {
redis.set(key, AssetDTO.empty(), Duration.ofMinutes(2));
return null;
}
redis.set(key, asset, Duration.ofMinutes(30).plusSeconds(random.nextInt(300)));
return asset;
}这个 Demo 体现了三个点:
- 查不到数据也短暂缓存空值,防穿透。
- 正常缓存 TTL 加随机时间,降低雪崩概率。
- 数据库仍然是事实来源,Redis 只是缓存副本。
更完整的生产版还应该包含:
- 参数校验,例如
assetId必须是正整数。 - 布隆过滤器,拦截明显不存在资产。
- 删除缓存失败重试,避免旧缓存长期存在。
- 热点资产互斥重建或逻辑过期。
- 监控缓存命中率、空值缓存数量、回源数据库 QPS。
商业 Demo:互斥锁防击穿
热点资产详情过期后,只允许一个线程回源数据库,其他线程稍等或返回旧值。
public AssetDTO getHotAsset(Long assetId) {
String dataKey = "asset:hot:" + assetId;
String lockKey = "lock:asset:hot:" + assetId;
AssetDTO cached = redis.get(dataKey, AssetDTO.class);
if (cached != null) {
return cached;
}
String requestId = UUID.randomUUID().toString();
boolean locked = redis.setIfAbsent(lockKey, requestId, Duration.ofSeconds(10));
if (!locked) {
sleep(50);
return redis.get(dataKey, AssetDTO.class);
}
try {
AssetDTO asset = assetRepository.findDetail(assetId);
redis.set(dataKey, asset, Duration.ofMinutes(30));
return asset;
} finally {
redis.deleteByLua(lockKey, requestId);
}
}为什么不能所有请求都回源:缓存击穿时,数据库承受的是热点流量的瞬时峰值。互斥锁的目标不是让请求更快,而是保护数据库不被打爆。
排查流程
当线上出现“接口慢、Redis timeout、数据库 QPS 飙升”时,可以这样判断:
flowchart TD
A["缓存相关故障"] --> B{"数据库 QPS 是否飙升"}
B -- "是" --> C["看缓存命中率"]
C --> D{"命中率是否下降"}
D -- "是" --> E["排查穿透、击穿、雪崩"]
D -- "否" --> F["可能是慢 SQL 或应用问题"]
B -- "否" --> G{"Redis 单节点是否压力高"}
G -- "是" --> H["排查热 Key、大 Key、慢命令"]
G -- "否" --> I["排查连接池、网络、序列化"]具体检查:
| 现象 | 优先看什么 |
|---|---|
| MySQL 空查询很多 | 穿透、参数校验、布隆过滤器、空值缓存 |
| 某个热点接口突然打 DB | 击穿、热点 Key 是否过期 |
| 大量接口同时回源 | 雪崩、TTL 集中、Redis 故障 |
| Redis 单节点 CPU 高 | 热 Key、复杂命令、Lua |
| Redis 慢日志有大集合命令 | 大 Key、全量命令 |
| 应用线程等待 Redis | 连接池耗尽、Redis timeout、网络 |
| 用户看到旧数据 | 缓存一致性、删除失败、本地缓存 |
常用命令:
redis-cli slowlog get 20
redis-cli info stats
redis-cli info commandstats
redis-cli info memory
redis-cli --bigkeys
redis-cli memory usage your:key应用侧指标同样重要:
- 缓存命中率。
- 回源数据库 QPS。
- Redis 连接池等待数。
- 空值缓存数量。
- 热点 key TopN。
- 删除缓存失败数。
练习
- 画出一个资产详情缓存流程。
- 设计缓存空值的过期时间。
- 写出缓存击穿的互斥锁流程。
- 思考哪些数据适合逻辑过期。
- 找一个可能的大 Key 设计拆分方案。
面试标准回答
缓存穿透、击穿、雪崩有什么区别
缓存穿透是查询数据库也不存在的数据,Redis 和 MySQL 都没有,每次请求都会打数据库,常用参数校验、空值缓存和布隆过滤器解决。缓存击穿是某个热点 Key 过期,大量请求同时未命中并回源数据库,常用互斥锁、逻辑过期和热点预热解决。缓存雪崩是大量 Key 同时过期或 Redis 整体故障,导致大量请求同时打到数据库,常用 TTL 随机、高可用、多级缓存、限流和降级解决。三者都可能让数据库压力暴涨,但原因不同,治理手段也不同。布隆过滤器和空值缓存怎么选
空值缓存适合重复查询同一个不存在的 Key,数据库查不到后把空结果以短 TTL 写入 Redis,下次相同请求就不会打数据库。布隆过滤器适合海量随机不存在 ID 的穿透攻击,它用位数组和多个哈希函数判断元素一定不存在或可能存在,内存占用小,但有误判、删除困难、需要初始化和增量同步。商业系统通常先做参数校验,再用布隆过滤器拦随机无效 ID,最后对少量漏过的不存在数据做空值缓存。逻辑过期适合哪些场景
逻辑过期适合读多写少、允许短时间旧数据的热点数据,例如商品详情、文章详情、字典配置、首页配置。它不是让 Redis Key 物理过期,而是在 value 中保存 expireAt,过期后先返回旧值,再由拿到锁的线程异步重建缓存。这样能避免热点 Key 过期瞬间大量请求打数据库。它不适合账户余额、支付状态、库存最终扣减、权限最终判断等强一致场景。Redis 缓存问题生产怎么排查
先看现象。如果数据库 QPS 飙升,要看缓存命中率是否下降,进一步判断穿透、击穿还是雪崩;如果 Redis 单节点 CPU 或网络高,要排查热 Key、大 Key、慢命令;如果应用报 Redis timeout,要同时看连接池等待、slowlog、网络、Redis CPU、命令耗时和数据库回源。常用命令包括 slowlog get、info commandstats、info memory、--bigkeys、memory usage。生产排查不能只看 Redis,要把应用、Redis、数据库和网络放在一条链路里看。关联知识点
| 知识点 | 继续学习什么 |
|---|---|
| 布隆过滤器原理 | 穿透治理、误判率、初始化和 Demo |
| 大 Key 与热 Key 全过程治理 | 大 Key、热 Key、Cluster 热点和治理 |
| Redis 高并发缓存治理 | 慢命令、连接池、限流降级、生产排查 |
| Redis 与数据库缓存一致性 | 写库删缓存、重试、CDC、TTL 兜底 |
| Redis 面试知识点 | 标准回答和追问 |
本章小结
Redis 缓存问题本质都是“缓存和数据库之间的流量保护问题”。穿透保护无效请求,击穿保护热点过期,雪崩保护大面积失效,热 Key 和大 Key 保护 Redis 自身稳定。
