notesArea/Redis/缓存击穿、穿透与雪崩.md

缓存击穿、穿透与雪崩

缓存击穿

大量并发请求同时访问一个过期的 key,这些请求会瞬间击穿缓存到达数据库,导致数据库负载。

解决办法

异步定时更新

某一热点数据过期时间为 1 小时,那么每 59 分钟去更新一下该热点数据,就不会存在 key 过期的情况了。

互斥锁

当某个 key 值为空时,在第一个请求访问该 key 时,给这个 key 加锁,然后去数据库读取数据并更新这个 key,更新完之后再解锁。在加锁期间,其余请求都会阻塞。

public String getData(String key) {
    String cacheKey = "cache:" + key;
    String lockKey = "lock:" + key;

    // 1. 查询缓存
    String value = redis.get(cacheKey);
    if (value != null) {
        return value;
    }

    // 2. 尝试加锁
    boolean locked = redis.set(lockKey, "1", "NX", "EX", 10); // 10秒过期
    if (locked) {
        try {
            // 当前线程负责查询 DB
            value = db.query(key);

            // 回填缓存,设置过期时间
            redis.set(cacheKey, value, 60);
        } finally {
            // 释放锁
            redis.del(lockKey);
        }
    } else {
        // 3. 其他线程休眠等待,再次读缓存
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {}
        return getData(key);
    }

    return value;
}

逻辑过期

缓存值里存一个 逻辑过期时间,即使 key 在 Redis 里没过期,但发现逻辑过期了:

  • 返回旧值给用户(不阻塞)
  • 后台线程去异步刷新缓存

这种方式避免了用户等待,但需要能容忍短时间的旧数据。

缓存穿透

请求到达缓存时,缓存没数据,然后请求绕过缓存查询数据库,此时数据库也没数据。当这类请求变多时缓存层失去了原本的作用。

缓存穿透的结果和 缓存击穿 一样,大量请求同时到达数据库,区别在于过程不一样:前者有缓存存在,但是没有值,只能再去查询;后者缓存不存在,直接去数据库查询。

解决办法

缓存空值

当第一查询数据库未查询到数据时,就把控制缓存下来,这样后续请求就不会绕过缓存查询数据库了。但是要注意缓存过期时间,如果数据库更新了,缓存再返回空值就不合适了。

此方法有两个弊端:

  1. 对空值做缓存,会导致 Redis 中缓存了更对的键,占用了更大的空间。此时如果遇到攻击,问题会更严重(大量的攻击请求产生大量空值键)。解决办法是给这类键设置一个较短失效时间
  2. 持久层和缓存层存在数据不一致的情况。解决办法是使用消息队列异步刷新缓存。

缓存雪崩

大量 key 同时过期或者缓存服务器宕机,导致请求直接到达数据库,造成数据库高负载。

解决办法

不同的过期时间

将不同的 key 的过期时间设置为不同,并定期通过刷新操作更新过期时间,这样就不会出现大量 key 同时过期的情况了。

集群

集群可以有效防止服务不可用的情况。

大纲
文件