缓存三问是少数"面试题 = 生产事故"高度重合的话题。这篇按"定义 → 解法 → 解法的副作用"把三个问题一次讲清。

先分清三个问题

问题 触发条件 本质
穿透 查询不存在的数据,缓存和 DB 都没有 恶意/无效流量直达数据库
击穿 某个热点 key 过期瞬间,高并发同时打到 DB 热点数据的瞬时失效
雪崩 大批 key 同时过期或缓存整体宕机 失效的规模效应

记忆法:穿透是"无中生有",击穿是"单点爆破",雪崩是"集体塌方"。

  flowchart TD
    A[请求] --> B{缓存命中?}
    B -->|命中| C[返回]
    B -->|未命中| D{key 存在过?}
    D -->|从未存在·穿透| E[无效流量直捣 DB]
    D -->|刚过期·击穿| F[热点并发直捣 DB]
    D -->|批量过期·雪崩| G[洪峰直捣 DB]
    E & F & G --> H[(数据库)]

穿透:两层防御

  1. 缓存空值:查无此 key 也写一个短 TTL(30-60s)的空标记。副作用:海量随机 key 会撑爆缓存——所以只对"key 空间可枚举"的业务用
  2. 入口校验:非法格式(ID 非数字、越界)在网关层直接拒绝。副作用:无,这是最便宜的防御,应该永远做

击穿:互斥重建

热点 key 失效时,只放一个请求去查库重建, others 等待:

value = redis.get(key)
if value is None:
    if redis.set(lock_key, 1, nx=True, ex=5):
        value = load_from_db(key)
        redis.set(key, value, ex=TTL)
        redis.delete(lock_key)
    else:
        time.sleep(0.05)
        return get(key)  # 重试读缓存

副作用:等待请求堆积;锁持有者挂了要靠锁超时兜底。逻辑过期是替代方案:value 里带过期时间,过期后返回旧值并异步重建——用户看到稍旧的数据,换取零等待。

雪崩:打散 + 兜底

  • TTL 加随机抖动:ttl = base + random(0, 300s),把同时过期摊开
  • 多级缓存:本地缓存(L1)挡在 Redis(L2)前面,Redis 整体挂掉时 L1 还能撑一部分
  • 熔断降级:DB 侧限流,宁可拒绝部分请求也不被打死——缓存层挂掉时保住数据库,就保住了恢复的希望

事后复盘的检查清单

每次缓存事故后核对:空值策略覆盖了吗 / 热点 key 有没有预热 / TTL 有没有抖动 / 降级预案演练过吗。缓存的问题从来不在缓存层本身,在于失效那一刻系统的行为——设计时永远多问一句:这一刻数据库看到什么?