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

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

参考资料

相关推荐
我爱娃哈哈2 小时前
SpringBoot + JSON 字段 + MySQL 8.0 函数索引:灵活存储半结构化数据,查询不慢
后端
赫瑞2 小时前
Java中的最长公共子序列——LCS
java·开发语言
于先生吖2 小时前
零基础开发国际版同城出行平台 JAVA 顺风车预约系统实战教学
java·开发语言
代码雕刻家2 小时前
2.22.StringBuffer类的常见用法、
java·开发语言
yhole2 小时前
Java进阶(ElasticSearch的安装与使用)
java·elasticsearch·jenkins
明月(Alioo)2 小时前
Python 并发编程详解 - Java 开发者视角
java·开发语言·python
0xDevNull3 小时前
基于Java的小程序地理围栏实现原理
java·小程序
arvin_xiaoting3 小时前
OpenClaw学习总结_II_频道系统_5:Signal集成详解
java·前端·学习·signal·ai agent·openclaw·signal-cli
凌波粒3 小时前
LeetCode--19.删除链表的倒数第 N 个结点(链表)
java·算法·leetcode·链表