缓存雪崩、缓存穿透、基于布隆过滤器解决缓存穿透问题、缓存击穿

缓存雪崩、缓存穿透、基于布隆过滤器解决缓存穿透问题、缓存击穿

编码文章call10242025-08-25 20:59:364A+A-

缓存雪崩

现象

缓存雪崩是指缓存数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库(redis主机挂了,Redis全盘崩溃、比如缓存中有大量数据同时过期)

解决方案

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
  • 设置热点数据永远不过期
  • 使用缓存预热
    缓存预热就是将数据提前加入到缓存中,当数据发生变更,再将最新的数据更新到缓存

缓存穿透

现象

请求去查询一条记录,先redis后mysql发现都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,这种现象我们称为缓存穿透,这个redis变成了一个摆设(redis中没有、mysql也没有)

解决方案一:空对象缓存或者缺省值

黑客会对你的系统进行攻击,拿一个不存在的id去查询数据,会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉

  • id相同打你系统:第一次打到mysql,空对象缓存后第二次就返回null了,避免mysql被攻击,不用再到数据库中去走一圈了
  • id不同打你系统:由于存在空对象缓存和缓存回写(看自己业务不限死),redis中的无关紧要的key也会越写越多(记得设置redis过期时间)


解决方案二:Google布隆过滤器Guava解决缓存穿透-单机版

/**
 * 测试布隆过滤器(可用于redis缓存穿透)
 * 
 */
public class TestBloomFilter {

    private static int total = 1000000;
    private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), total);

    public static void main(String[] args) {
        // 初始化ID数据到过滤器中
        for (int i = 0; i < total; i++) {
            bf.put(i);
        }

        for (int i = 0; i < total; i++) {
            if (!bf.mightContain(i)) {
                // todo 
            }
        }

    }

}

解决方案三:Redis布隆过滤器解决缓存穿透

Guava 提供的布隆过滤器的实现还是很不错的 (想要详细了解的可以看一下它的源码实现),但是它有一个重大的缺陷就是只能单机使用 ,而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到Redis中的布隆过滤器了

注:Redis在4.0 之后有了插件功能(Module),可以使用外部的扩展功能,可以使用RedisBloom作为Redis布隆过滤器插件。

实现案例

缓存击穿

现象

缓存击穿:大量的请求同时查询一个key时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去(简单说就是热点key突然失效了,暴打mysql)

危害:会造成某一时刻数据库请求量过大,压力剧增

方案一:互斥更新、随机退避、差异失效时间

方案2:对于访问频繁的热点key,干脆就不设置过期时间

方案3:互斥独占锁防止击穿

public User findUserById2(Integer id) {
    User user = null;
    String key = CACHE_KEY_USER+id;

    //1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
    user = (User) redisTemplate.opsForValue().get(key);
    if(user == null) {
        //2 大厂用,对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql
        synchronized (UserService.class){
            user = (User) redisTemplate.opsForValue().get(key);
            //3 二次查redis还是null,可以去查mysql了(mysql默认有数据)
            if (user == null) {
                //4 查询mysql拿数据
                user = userMapper.selectByPrimaryKey(id);//mysql有数据默认
                if (user == null) {
                    return null;
                }else{
                    //5 mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
                    //setnx 不存在才创建
                    redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
                }
            }
        }
    }
    return user;
}
点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4