缓存三大问题的万恶之源是同一句话:缓存未命中,请求落到了数据库。 区别只在“为什么未命中”:

问题原因典型场景对策
穿透查询根本不存在的数据,缓存永远不可能命中恶意用 id=-1 或随机 UUID 高频打接口空值缓存 / 布隆过滤器 / 参数校验
击穿某个热点 key 恰好过期的瞬间,并发全部落库爆款商品详情缓存到期的第 1 秒互斥锁重建 / 逻辑过期 / 热点永不过期
雪崩大量 key 同时过期或 Redis 整体不可用凌晨批量预热的缓存同一秒 TTL 到期TTL 加随机值 / 集群高可用 / 限流降级

穿透:缓存一个“短的空值”

val := GET(key)
if val == nil {
    db := QueryDB(key)
    if db == nil {
        SET(key, null, EX 60)   // 空值也要缓存,TTL 短一点
        return NotFound
    }
    SET(key, db, EX 3600)
}
return val

空值 TTL 要短(防止新数据写入后长期读到空),同时入库前对 id 做格式校验,绝大多数恶意请求在参数层就该被拒绝。数据量大且 key 无穷多时再上布隆过滤器:判断“不存在”一定准确,能把恶意流量挡在 Redis 之前。

击穿:一把锁放一个人进去重建

val := GET(key)
if val == nil {
    if SETNX(lock_key, 1, EX 5) {  // 抢到锁的请求
        db  := QueryDB(key)
        SET(key, db, EX 3600)
        DEL(lock_key)
    } else {                        // 没抢到的稍后重试
        SLEEP(50ms); retry()
    }
}

代价是重建期间其他请求要短暂等待。对一致性要求松的场景可以用“逻辑过期”:value 里带过期时间,过期后返回旧值并异步重建,永不阻塞。

雪崩:别让所有 key 排队过期

批量预热时给 TTL 加随机抖动(如 3600 + rand(0,600)),把过期时间摊开;再往上还有两道保险:Redis 集群保可用性,应用侧限流熔断保数据库最后一口气。缓存的定位是数据库的伞——伞可以破,但别让雨一瞬间全浇下来。