Skip to content

Redis 缓存问题

Redis 常被用作缓存,用来减轻数据库压力、提升接口响应速度。但缓存不是简单地“查 Redis,没有再查数据库”。如果设计不好,会出现缓存穿透、击穿、雪崩、热 Key、大 Key 等问题。

学习目标

学完这一页,你要能做到:

  1. 分清缓存穿透、击穿、雪崩、大 Key、热 Key 的本质区别。
  2. 画出 Cache Aside 的正常读流程,以及异常流量如何打穿数据库。
  3. 解释空值缓存、布隆过滤器、互斥锁、逻辑过期、TTL 随机分别解决什么问题。
  4. 说清楚为什么这些方案都不是银弹,以及不这样做会出现什么线上事故。
  5. 能为资产详情、商品详情、字典配置、订单查询这类商业场景选择合适方案。
  6. 能写出基础 Demo,并知道真实项目还要加重试、限流、监控和降级。

缓存查询基本流程

mermaid
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 高并发缓存治理

这些问题的本质区别

很多人背题时会把穿透、击穿、雪崩混在一起。可以先按“压力为什么绕过缓存”来理解:

mermaid
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;慢日志出现 HGETALLSMEMBERSLRANGE 0 -1,优先想大 Key。

缓存穿透

缓存穿透指查询一个数据库里也不存在的数据。因为缓存没有,数据库也没有,所以每次请求都会打到数据库。

示例:

text
请求 articleId = -1
Redis 没有
MySQL 也没有
下次请求仍然重复打 MySQL

方案一:缓存空值

数据库查不到时,也在 Redis 中缓存一个空结果,设置较短过期时间。

text
article:-1 -> null,过期时间 1 分钟

优点:简单。

缺点:如果无效 Key 非常多,会占用 Redis 空间。

方案二:布隆过滤器

布隆过滤器可以快速判断一个 ID 是否“可能存在”。

mermaid
flowchart TD
    A["请求 ID"] --> B["布隆过滤器判断"]
    B --> C{"可能存在吗"}
    C -- "否" --> D["直接拒绝或返回空"]
    C -- "是" --> E["继续查缓存和数据库"]

布隆过滤器可能误判存在,但不会把一定不存在的数据放过去太多。它的底层是“位数组 + 多个哈希函数”:添加元素时把多个 bit 置为 1,查询时只要有一个 bit 为 0,就说明一定不存在;如果所有 bit 都是 1,只能说明可能存在。完整原理、误判原因、参数估算和 Demo 看:布隆过滤器原理

穿透治理怎么选

请求类型推荐方案原因
明显非法参数参数校验直接拒绝成本最低,不要把垃圾流量放进缓存层
重复查询同一个不存在 ID空值缓存简单有效,短 TTL 即可
大量随机不存在 ID布隆过滤器空值缓存会产生海量无效 key,布隆过滤器更省内存
恶意攻击流量限流 + 黑名单 + 布隆过滤器只靠缓存方案不够
新增数据频繁布隆过滤器要可靠增量更新否则新数据可能被误拦

商业系统通常组合使用:

mermaid
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 空值"]

不这样做会怎样:

  1. 攻击者随机构造 ID,Redis 永远不命中。
  2. 每个请求都查 MySQL。
  3. MySQL 连接池被无效查询占满。
  4. 正常用户请求也变慢。
  5. 应用线程堆积,接口超时。

缓存击穿

缓存击穿指一个热点 Key 过期,大量请求同时发现缓存没有,于是一起打数据库。

例如热门商品、数据资产详情、字典配置、采集任务状态这类热点缓存过期,瞬间上千请求同时查数据库。

mermaid
flowchart TD
    A["大量请求访问热点 Key"] --> B{"缓存是否过期"}
    B -- "是" --> C["竞争重建锁"]
    C --> D{"是否拿到锁"}
    D -- "是" --> E["查询数据库并写缓存"]
    D -- "否" --> F["等待、重试或返回旧值"]

方案一:互斥锁

只有拿到锁的线程去查数据库并重建缓存,其他线程等待或稍后重试。

优点:避免数据库被打爆。

缺点:实现复杂一些,等待会增加延迟。

方案二:逻辑过期

缓存数据本身不过期,而是在值里保存逻辑过期时间。

json
{
  "data": "...",
  "expireAt": "2026-06-27 10:00:00"
}

如果逻辑过期,先返回旧值,再异步重建缓存。

适合读多写少、允许短时间旧数据的场景。

互斥锁和逻辑过期怎么选

方案优点缺点适合
互斥锁数据更接近实时,只让一个线程回源等待线程延迟增加,锁要防误删和超时详情页、普通热点缓存
逻辑过期用户请求不等待数据库,抗峰值好会短暂返回旧数据商品详情、文章详情、字典配置
永不过期 + 主动刷新热点稳定,延迟低主动刷新失败会旧值长期存在首页配置、榜单快照
直接查库实现简单热点过期会打爆数据库低并发或非热点

互斥锁流程:

mermaid
flowchart TD
    A["热点 Key 未命中"] --> B["尝试 set nx 获取重建锁"]
    B --> C{"是否拿到锁"}
    C -- "是" --> D["查 MySQL"]
    D --> E["写 Redis"]
    E --> F["释放锁"]
    C -- "否" --> G["短暂 sleep 或返回旧值"]
    G --> H["再次查 Redis"]

逻辑过期流程:

mermaid
flowchart TD
    A["读取 Redis 数据"] --> B{"逻辑过期了吗"}
    B -- "否" --> C["直接返回"]
    B -- "是" --> D["尝试获取重建锁"]
    D --> E{"是否拿到锁"}
    E -- "是" --> F["异步重建缓存"]
    E -- "否" --> G["直接返回旧值"]
    F --> H["更新数据和逻辑过期时间"]

注意:逻辑过期不是强一致方案。账户余额、支付状态、库存最终扣减、权限最终判断,不应该为了抗击穿就随便返回旧值。

缓存雪崩

缓存雪崩指大量 Key 同时失效,或者 Redis 整体不可用,导致大量请求同时打到数据库。

常见原因:

  1. 缓存过期时间都设置成 30 分钟。
  2. 批量预热时同一时间写入。
  3. Redis 集群故障。

解决方式:

  1. 过期时间加随机值。
  2. 热点数据提前预热。
  3. Redis 高可用。
  4. 接口限流和降级。
  5. 多级缓存。

示例:

java
int ttl = 3600 + RandomUtils.nextInt(0, 300);

雪崩为什么比击穿更危险

击穿通常是一个热点 Key 过期,雪崩是大量 Key 或整个 Redis 层不可用。

mermaid
flowchart TD
    A["大量 Key 同时过期"] --> B["大量请求同时未命中"]
    B --> C["数据库连接池被打满"]
    C --> D["SQL 变慢"]
    D --> E["应用线程等待"]
    E --> F["接口超时扩大"]

雪崩治理要分两类:

类型原因治理
过期雪崩TTL 设置太集中TTL 随机、分批预热、热点不过物理过期
故障雪崩Redis 节点或网络异常高可用、熔断、限流、降级、本地兜底

Redis 异常时,不能让所有请求无脑查数据库。成熟系统要限制回源并发:

mermaid
flowchart TD
    A["Redis 异常"] --> B["缓存访问快速失败"]
    B --> C["判断是否核心接口"]
    C -- "非核心" --> D["降级返回兜底"]
    C -- "核心" --> E["限量回源数据库"]
    E --> F["保护 DB 连接池"]

否则 Redis 只是“坏了一个组件”,但数据库被打爆后会变成“整站不可用”。

热 Key

热 Key 指某个 Key 被极高频访问。

危害:

  1. 单个 Redis 节点压力过大。
  2. 网络流量集中。
  3. 请求延迟升高。

处理方式:

  1. 本地缓存一份短时间数据。
  2. 拆分 Key。
  3. 热点数据预热。
  4. 对热点接口限流。
  5. 监控 Key 访问频率。

热 Key 的完整识别、Cluster 热点、本地缓存、逻辑过期和限流方案看:大 Key 与热 Key 全过程治理

热 Key 和缓存击穿经常同时出现:

  1. 某个商品详情是热 Key。
  2. Redis 能扛住正常读。
  3. 这个 Key 到期或被删除。
  4. 所有请求同时回源数据库。
  5. 热 Key 变成击穿事故。

所以热点数据要么提前预热,要么逻辑过期,要么用互斥锁重建。不能只设置普通 TTL 就不管了。

大 Key

大 Key 指单个 Key 的 value 很大,例如:

  1. 一个 String 存 5MB JSON。
  2. 一个 List 有几十万元素。
  3. 一个 Hash 有大量字段。

危害:

  1. 网络传输慢。
  2. 删除阻塞。
  3. Redis 内存不均衡。
  4. 操作耗时长。

处理方式:

  1. 拆分成多个 Key。
  2. 限制集合大小。
  3. 使用分页。
  4. 删除时使用异步删除。
  5. 定期扫描大 Key。

大 Key 的阈值判断、扫描命令、拆分模型、异步删除和生产风险看:大 Key 与热 Key 全过程治理

大 Key 和缓存雪崩也可能有关:大量大 Key 同时过期或被删除时,Redis 清理、AOF rewrite、主从复制和网络都会抖动。所以大 Key 治理不只是“节省内存”,也是稳定性治理。

生产建议

  1. 缓存空值设置短 TTL。
  2. 热点 Key 不要使用统一过期时间。
  3. 缓存重建要防并发击穿。
  4. Redis 异常时要限流保护数据库。
  5. 监控命中率、慢命令、内存、热 Key、大 Key。
  6. 缓存数据要能容忍短时间不一致。

商业场景选型

场景主要风险推荐组合
资产详情穿透、击穿、旧数据参数校验 + 布隆过滤器 + Cache Aside + 写库后删缓存 + TTL
商品详情热 Key、击穿、雪崩预热 + 本地缓存 + 逻辑过期 + 限流
字典配置热 Key、本地缓存一致性Redis + Caffeine + 短 TTL + 变更广播
订单查询读己之写、强一致写后读库或返回最新值,缓存只做展示加速
搜索筛选缓存组合爆炸ES 或搜索服务,不要无限拼 Redis key
统计看板大范围聚合汇总表或离线任务,Redis 存结果快照
不存在 ID 查询穿透参数校验 + 空值缓存 + 布隆过滤器

选型时先问三个问题:

  1. 这个数据能不能短暂旧?
  2. 这个 Key 会不会变大或变热?
  3. Redis 异常时,能不能查数据库,查多少,查不到返回什么?

商业 Demo:Cache Aside 查询资产详情

Cache Aside 是商业项目最常用的缓存模式:应用先查缓存,缓存没有再查数据库,然后把结果写回缓存。

java
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 体现了三个点:

  1. 查不到数据也短暂缓存空值,防穿透。
  2. 正常缓存 TTL 加随机时间,降低雪崩概率。
  3. 数据库仍然是事实来源,Redis 只是缓存副本。

更完整的生产版还应该包含:

  1. 参数校验,例如 assetId 必须是正整数。
  2. 布隆过滤器,拦截明显不存在资产。
  3. 删除缓存失败重试,避免旧缓存长期存在。
  4. 热点资产互斥重建或逻辑过期。
  5. 监控缓存命中率、空值缓存数量、回源数据库 QPS。

商业 Demo:互斥锁防击穿

热点资产详情过期后,只允许一个线程回源数据库,其他线程稍等或返回旧值。

java
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 飙升”时,可以这样判断:

mermaid
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、网络
用户看到旧数据缓存一致性、删除失败、本地缓存

常用命令:

bash
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

应用侧指标同样重要:

  1. 缓存命中率。
  2. 回源数据库 QPS。
  3. Redis 连接池等待数。
  4. 空值缓存数量。
  5. 热点 key TopN。
  6. 删除缓存失败数。

练习

  1. 画出一个资产详情缓存流程。
  2. 设计缓存空值的过期时间。
  3. 写出缓存击穿的互斥锁流程。
  4. 思考哪些数据适合逻辑过期。
  5. 找一个可能的大 Key 设计拆分方案。

面试标准回答

缓存穿透、击穿、雪崩有什么区别

text
缓存穿透是查询数据库也不存在的数据,Redis 和 MySQL 都没有,每次请求都会打数据库,常用参数校验、空值缓存和布隆过滤器解决。缓存击穿是某个热点 Key 过期,大量请求同时未命中并回源数据库,常用互斥锁、逻辑过期和热点预热解决。缓存雪崩是大量 Key 同时过期或 Redis 整体故障,导致大量请求同时打到数据库,常用 TTL 随机、高可用、多级缓存、限流和降级解决。三者都可能让数据库压力暴涨,但原因不同,治理手段也不同。

布隆过滤器和空值缓存怎么选

text
空值缓存适合重复查询同一个不存在的 Key,数据库查不到后把空结果以短 TTL 写入 Redis,下次相同请求就不会打数据库。布隆过滤器适合海量随机不存在 ID 的穿透攻击,它用位数组和多个哈希函数判断元素一定不存在或可能存在,内存占用小,但有误判、删除困难、需要初始化和增量同步。商业系统通常先做参数校验,再用布隆过滤器拦随机无效 ID,最后对少量漏过的不存在数据做空值缓存。

逻辑过期适合哪些场景

text
逻辑过期适合读多写少、允许短时间旧数据的热点数据,例如商品详情、文章详情、字典配置、首页配置。它不是让 Redis Key 物理过期,而是在 value 中保存 expireAt,过期后先返回旧值,再由拿到锁的线程异步重建缓存。这样能避免热点 Key 过期瞬间大量请求打数据库。它不适合账户余额、支付状态、库存最终扣减、权限最终判断等强一致场景。

Redis 缓存问题生产怎么排查

text
先看现象。如果数据库 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 自身稳定。