简单聊聊缓存雪崩、穿透、击穿

70 阅读3分钟

1. 缓存雪崩

1.1 描述:

即大量缓存同一时间大面积的失效,这个时候来了一大波请求,都打到数据库上,最后数据库处理不过来崩了。

1.2 解决方案:

1.2.1 分析原因:
  • 大批缓存同时过期
  • Redis 故障宕机,缓存系统异常
1.2.2 针对缓存批量过期:
  • 分散缓存失效时间
    给 key设置不同的时间,让其过期,避免同一时间大批量过期,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
1.2.3 针对 redis 故障
  • 服务熔断和接口限流;
  • 构建高可用缓存集群系统。

2. 缓存击穿

2.1 描述:

热点key 在某一时间失效,导致大量请求直接打到了DB 服务器,导致数据库宕机。

2.2 解决方案:

2.2.1 设置缓存永不过期
2.2.2 加分布式锁

分布式锁的详细实现参考:juejin.cn/post/693695…

第一个请求的线程可以拿到锁,拿到锁的线程查询到数据之后设置缓存,其他的线程获取锁失败后会等待50ms,然后重新到缓存中获取数据,这样就可以避免大量的请求落到数据库中。

public Object getData(String key, String requestId) throws InterruptedException {
    Object value = redis.get(key);
    // 缓存值过期
    if (value == null) {
        // requestId:全局唯一请求id,可以使用 UUID.randomUUID().toString().replace("-", "").toLowerCase();
        // requestId 作为锁键的值
        String lockKey = "lock." + key
        if (redis.set(lockKey, requestId, "PX", lockExpire, "NX")) {
            try {
                // 查询数据库,并写到缓存,让其他线程可以直接走缓存
                value = getDataFromDb(key);
                redis.set(key, value, "PX", expire);
            } catch (Exception e) {
                // 异常处理
            } finally {
                // 释放锁, 判断 request id 是否一致和删除锁两个操作要保持原子性
                String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " + 
                "return redis.call('del',KEYS[1]) else return 0 end"; 
                return jedis.eval(luaScript, 
                Collections.singletonList(lockKey), 
                Collections.singletonList(requestId)).equals(1L);
            }
        } else {
            // sleep50ms后,进行重试
            Thread.sleep(50);
            return getData(key, requestId);
        }
    }
    return value;
}

3. 缓存穿透

3.1 描述:

大量请求请求了不存在的key,直接打到数据库上,导致数据库宕机。

3.2 解决方案:

3.3 布隆过滤器

将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对数据库的查询压力。 java 使用布隆过滤器有三种方式:

  1. 谷歌的Guava, 只支持单体应用,使用本地内存,不便于分布式系统数据同步
  2. java 自己使用bitmap 实现, 使用本地内存,不便于分布式系统数据同步
  3. Redisson 组件实现,支持分布式,推荐使用

布隆过滤器的详细解析参考

3.4 设置空缓存

如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴!