缓存穿透、击穿、雪崩:三种问题三种解法

米阳 2022-3-6 4 3/6

这三个词经常被混着说,但成因完全不同,处理方式也不一样。分清楚之后,线上出事才知道该动哪里。

穿透:查一个根本不存在的数据

请求带着不存在的 id 打进来,缓存没有,落到数据库也没有,于是每次请求都穿透到 DB。如果有人拿随机 id 刷接口,数据库很快就顶不住。

两种处理:

  1. 缓存空值。查不到也写一个短 TTL 的空标记(比如 60 秒),下次同样的 id 直接在缓存层返回。实现最简单,代价是要防住大量不同 id 的攻击——那就得配合限流。
  2. 布隆过滤器。把所有存在的 id 预热进去,请求先过过滤器,明确不存在的直接拒掉。注意布隆过滤器有假阳性没有假阴性,说“不存在”一定不存在,说“存在”可能误判。

击穿:热点 key 过期的瞬间

某个商品详情页 QPS 很高,缓存 TTL 到期的同一秒,几千个请求同时发现缓存没了,一起去查库。数据库瞬间被打出连接池上限。

解法是加锁,只放一个请求去回源:

def get(key):
    v = redis.get(key)
    if v is not None:
        return v
    lock = redis.set(f"lock:{key}", "1", nx=True, ex=5)
    if lock:
        try:
            v = db_query(key)
            redis.set(key, v, ex=300)
        finally:
            redis.delete(f"lock:{key}")
        return v
    time.sleep(0.05)
    return get(key)  # 等待后重试,实际要加最大次数限制

更省事的做法是逻辑过期:value 里存一个业务过期时间,物理上不过期,发现逻辑过期就异步刷新,先返回旧值。牺牲一点一致性换稳定性。

雪崩:大量 key 同时失效

要么是统一预加载、TTL 设成同一个值,要么是 Redis 整个挂了。前者在 TTL 上加随机抖动就能解决:

expire = 300 + random.randint(0, 60)

后者要靠架构:Redis 主从 + 哨兵、本地二级缓存兜底、DB 前的限流。真挂了的时候,能返回降级页面比全部超时要好。

顺序别搞反

先分清是哪种问题,再动手。给所有 key 加随机 TTL 解决不了穿透,加布隆过滤器也挡不住雪崩。我见过一个项目三种方案全上了一层,结果缓存逻辑复杂到没人敢改——问题其实只有一个:热点 key 的过期时间太集中。

- THE END -

米阳

3月06日15:20

最后修改:2022年3月6日
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论