工程实践 · 2021年4月9日 · 2 分钟

Redis 缓存实战:击穿、穿透、雪崩全搞定

缓存三大经典问题的场景分析与解法:互斥锁、布隆过滤器、随机 TTL 与集群容灾。

上篇 Redis 入门里写了基本操作,这次聊聊缓存里最头疼的三个问题。先说个真实场景——公司有个活动页面,查的是热门商品的库存,流量一大数据库就扛不住。加了缓存之后好些了,结果有一天活动刚上线,缓存全挂了。

缓存击穿

现象:一个热点 key 过期了,瞬间大量请求打到数据库。

实际的画面是这样的:缓存 key 到期→第一个请求过来缓存 miss→去查数据库→还没查完第二个请求又来了,缓存还是 miss→十几个请求全打在数据库的同一行数据上。

解法:最简单的就是用互斥锁,拿到锁的去查数据库,其他的等着:

String get(String key) {
    String val = redis.get(key);
    if (val != null) return val;

    String lockKey = "lock:" + key;
    if (redis.setnx(lockKey, "1")) {
        redis.expire(lockKey, 30);
        val = db.query(key);
        redis.set(key, val, 300);
        redis.del(lockKey);
        return val;
    }

    Thread.sleep(100);
    return get(key);  // 重试
}

核心思路:让缓存 miss 之后只有一个线程去加载数据。缺点是代码复杂度上来了,而且第一个请求要等数据库查询。

缓存穿透

现象:客户端查一个根本不存在的数据,缓存和数据库都没有,每次请求都穿过去打数据库。

比如有人拿 userId=-1 一直刷接口,缓存里没有 -1,数据库也没有 -1,每次都会走到数据库查询那一步。

解法:布隆过滤器或者缓存空值。

         请求 userId=-1
              │
              ▼
    ┌──────────────────┐
    │  布隆过滤器       │
    │  是否存在此 ID?   │
    └────┬────────┬────┘
         │YESNO
         ▼        ▼
     查缓存    直接返回空

布隆过滤器可以告诉你"这个 key 一定不存在",但会误判"可能存在"。适合用来挡掉大量不存在的 key。

更简单的方案是缓存空值,redis.set("user:-1", "null", 60),60 秒过期。缺点是被恶意攻击时空值也会占内存。

缓存雪崩

现象:大量缓存 key 在同一时间过期,或者 Redis 宕机。

和击穿的区别:击穿是一个热点 key 过期,雪崩是一大片 key 同时过期。

解法有两个方向

  1. 过期时间加随机值:原本统一 5 分钟过期,改成 5 分钟 ± 随机 60 秒,避免集中在同一时刻失效。
int ttl = 300 + ThreadLocalRandom.current().nextInt(60);
redis.set(key, value, ttl);
  1. Redis 集群 + 熔断降级:Redis 不要单机部署,用哨兵或集群模式。如果 Redis 真的全挂了,就打开限流开关直接返回兜底数据,不要把流量全部压到数据库。

写在最后

三个"缓"问题归根到底是一个思路:**永远不要信任缓存。**缓存是数据库的护城河,但护城河自己也可能出事。每次做缓存方案的时候都要问自己:如果缓存挂了或没有命中,数据库抗不扛得住?

代码上的各种锁、布隆、随机 TTL,本质上都是为了让缓存"可预期的失败",而不是放任流量乱打。

继续阅读

评论