Redis 缓存三大经典问题:穿透、击穿与雪崩

一、缓存穿透(Cache Penetration)

1.1 是什么

缓存穿透 一般指:请求所带的标识(如用户 ID)在缓存里查不到 ,于是落到数据库 ;而数据库里同样不存在 这条数据,因此也无法回写 一条「有意义的缓存记录」。结果是:同一类恶意或异常请求会反复穿透缓存,每次都打数据库

若这类请求量很大,数据库可能在短时间内承受远超平时的 QPS,存在被打垮的风险。

1.2 应对思路

(1)参数校验,尽早拦截非法请求

对入参做业务规则校验,从源头减少「注定查不到」的请求。

例如:合法用户 ID 约定为 15xxxxxx 形态,则对 16232323 这类明显不符合规则的 ID 可直接返回错误,不再访问缓存与数据库。这能过滤一部分伪造或扫库的恶意请求。

(2)布隆过滤器(Bloom Filter)

原理简述 :底层用 bit 数组 表示集合;初始化时把数据库中已存在的 key 经多次哈希(如三次)映射到多个下标,并将对应位置置为 1。查询时同样做哈希,若相关位不全为 1,则可判定「一定不存在」,从而避免无意义的数据库查询。

能解决:大量「确实不存在」的请求在过滤器层被挡掉,减轻数据库压力。

需注意的两点

问题 说明
误判 哈希存在冲突,不同 key 可能映射到相同位置,存在「假阳性」------过滤器认为可能存在,实际库中仍没有。通常可通过调整位数组大小与哈希次数权衡。
数据更新与一致性 布隆过滤器与数据库是两套数据源 。例如库中新增 了用户,若同步到布隆过滤器失败(网络、任务延迟等),可能出现:合法用户被误判为不存在而遭拦截。因此要有可靠的增量同步、补偿或降级策略。

(3)缓存空值(Cache Null)

当缓存未命中且数据库也查不到时,仍将该 key 写入缓存 ,值为空或占位(可配合较短 TTL,避免长期占用内存)。

后续相同 key 的请求可直接在缓存层得到「空结果」,不再重复查库


二、缓存击穿(Cache Breakdown)

2.1 是什么

缓存击穿 多指:热点 key 在某一时刻过期失效 ,此时大量并发请求同时未命中缓存,一齐涌向数据库,造成瞬时压力骤增,甚至拖垮数据库。

与「穿透」的区别:击穿场景下,数据在库里一般是存在的,只是缓存这一层暂时失效。

2.2 应对思路

(1)互斥锁 / 单飞(Single Flight)

压力来自「同一时刻过多请求同时打库」。可对同一个热点 key (如同一个 productId)加锁:同一时刻只允许一个线程/请求去查库并回写缓存,其余请求短暂等待后读缓存或重试。

(2)自动续期

击穿与 key 物理过期 强相关。可在过期前主动刷新:例如定时任务每隔 20 分钟重建缓存并把 TTL 重新设为 30 分钟,使热点数据在业务高峰期内始终有效。

(3)热点 key 不设物理过期 + 预热

数量可控 的热点(如秒杀商品 ID),可不设置 Redis TTL ,在活动前预热 写入缓存,活动结束后手动删除无用 key,从根本上避免「到期瞬间集体失效」。

(4)逻辑过期时间

思路 :Redis 中的 key 不设或使用很长的物理 TTL ,在 value 内 携带逻辑过期时间戳 ;读取时若判断已逻辑过期,则异步刷新 缓存,当前请求仍返回旧数据(业务可接受短暂旧读的前提下),避免大量线程同时阻塞在数据库上。

缓存实体示例

java 复制代码
public class CacheData<T> {
    private T value;           // 实际数据
    private long expireTime;   // 逻辑过期时间戳(毫秒)

    public CacheData(T value, long expireSeconds) {
        this.value = value;
        this.expireTime = System.currentTimeMillis() + expireSeconds * 1000;
    }

    public boolean isExpired() {
        return System.currentTimeMillis() > expireTime;
    }

    public T getValue() { return value; }
    public long getExpireTime() { return expireTime; }
}

写入缓存(不依赖 Redis TTL 表达业务过期)

java 复制代码
@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
private ObjectMapper objectMapper;

public void setCache(String key, String value, long expireSeconds) throws Exception {
    CacheData<String> cacheData = new CacheData<>(value, expireSeconds);
    String json = objectMapper.writeValueAsString(cacheData);
    // 不设置 TTL,或仅作兜底;业务过期由 expireTime 控制
    redisTemplate.opsForValue().set(key, json);
}

读取:逻辑过期则异步更新,仍返回旧值

java 复制代码
public String getCache(String key) throws Exception {
    String json = redisTemplate.opsForValue().get(key);
    if (json == null) {
        return null;
    }
    CacheData<String> data = objectMapper.readValue(json,
        new TypeReference<CacheData<String>>() {});

    if (data.isExpired()) {
        asyncUpdate(key);  // 异步重建缓存,避免同步打满数据库
    }
    return data.getValue();
}

三、缓存雪崩(Cache Avalanche)

3.1 是什么

可理解为缓存击穿在规模上的放大

  • 击穿 :往往聚焦在单个热点 key 失效后的并发打库。
  • 雪崩大量 key 在同一时间段失效 ,或缓存集群整体不可用,导致请求集中落到数据库或下游,风险更大。

常见两类场景:

  1. 批量 key 同时过期:例如同一批缓存使用了相同或接近的 TTL,到期时刻重叠。
  2. 缓存服务故障 :单机故障、集群脑裂、机房网络问题等导致整层缓存不可用,所有读请求穿透到数据库。

3.2 应对思路

(1)过期时间加随机偏移

在基准 TTL 上增加随机秒数 (如 1~60 秒),从而打散大量 key 的失效时间点,降低同一瞬时打库的概率。

(2)服务降级与熔断

在应用侧维护全局或按资源的降级开关 :例如监测到「最近一分钟内 Redis 连续失败达到阈值」,则打开降级,后续请求返回默认值、静态页或简化数据,避免把数据库拖死。

可与配置中心、限流、熔断组件结合使用。

参考资料

相关推荐
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第87题】【Mysql篇】第17题:分布式事务的实现原理?
java·数据库·分布式·mysql·面试
红尘散仙2 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
来杯@Java2 小时前
图书管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·mybatis·课程设计
卷毛的技术笔记3 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
编程大师哥3 小时前
匿名函数 lambda + 高阶函数
java·python·算法
会编程的土豆3 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木3 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
Cosolar3 小时前
从零写一个 Attention Is All You Need
人工智能·面试·架构
adrninistrat0r3 小时前
Java调用链MCP分析工具
java·python·ai编程
喵个咪3 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm