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 连续失败达到阈值」,则打开降级,后续请求返回默认值、静态页或简化数据,避免把数据库拖死。

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

参考资料

相关推荐
葫芦和十三4 小时前
图解 MongoDB 07|索引类型:七种索引,七种访问形状
后端·mongodb·agent
朦胧之6 小时前
AI 编程-老项目改造篇
java·前端·后端
swipe8 小时前
从 0 到 1 实现大文件上传:分片、秒传、断点续传、暂停、重试与服务端合并
前端·javascript·面试
爱勇宝8 小时前
我做了一个只用来搜歌词的小 App
android·前端·后端
IT_陈寒9 小时前
SpringBoot自动配置坑了我一晚上,原来问题出在这
前端·人工智能·后端
SelectDB10 小时前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
SelectDB10 小时前
秒级弹性、最高降本 70%:SelectDB Serverless 如何重塑云数仓资源效率
大数据·后端·云原生
程序猿大帅10 小时前
别再只当调包侠了:用 Spring AI 落地 Function Calling,我被大模型硬生生砸出了三个大坑
java
PinkSun10 小时前
Spring AI ChatMemory踩坑实录:重启丢数据、Agent丢记忆、对话溢出
后端·ai编程
壹方秘境10 小时前
我用Go语言开发了一个跨平台的HTTPS抓包和调试工具
前端·后端·ios