Redis的分布式缓存问题

穿透

  数据库与redis都不存在的key,由于莫名原因存在大量请求,导致请求跳过redis而访问DB

处理方法:

  1. 数据库不存在,redis也存储一个 null(或者常熟),并设置一个较短的过期时间,防止跳过redis
  2. 布隆过滤器

击穿

  Redis曾存在的key,由于过期时间而被删除,导致请求跳过redis而访问DB

处理方法:

  1. 不设置过期时间,永远存在
  2. 使用锁,synchronized、分布式锁

雪崩

  多个不同的key,由于设置了相同的过期时间,导致多个key在同一时间而被删除,从而导致DB压力过大

处理方法:

  1. 对每个key的过期时间,通过RandomUtils.nextInt(10)+1创建一个随机时间

代码功能实现:穿透、击穿、雪崩

    /**
     * 使用 redis 进行缓存
     * @return
     */
    @Override
    public Map<String, List<Catalog2VO>> getCatelog2JSON() {

        System.out.println("为了严谨性,添加双重判断的 第一个判断");
        String catelogs = stringRedisTemplate.opsForValue().get(CATEGORY_KEYS);
        if (Objects.equals(catelogs,"0")) {
            return null;
        }

        if (Strings.isNullOrEmpty(catelogs)){
            Map<String, List<Catalog2VO>> catelog2JSONForDB = null;
            System.out.println("添加锁功能,是为了防止缓存击穿");
            String uuid = UUID.randomUUID().toString();
            // 防止查询数据库存在问报错,而导致没有解锁的问题。所以加入超时时间
            // 使用 nx命令,保证判断与赋值为原子性,属于分布式锁
            if(stringRedisTemplate.opsForValue().setIfAbsent("catelogLock", uuid,30, TimeUnit.SECONDS)) {
                try{
                    catelog2JSONForDB = getCatelog2JSONForDB();
                    return catelog2JSONForDB;
                } finally {
                    System.out.println("缓存击穿添加的锁,现在进行解锁(原子性)");
                    // 通过Redis的lua脚本实现 查询和删除操作的原子性
                    String srcipts = "if redis.call('get',KEYS[1]) == ARGV[1]  then return redis.call('del',KEYS[1]) else  return 0 end ";
                    stringRedisTemplate.execute(new DefaultRedisScript<Long>(srcipts,Long.class)
                            ,Arrays.asList("lock"),uuid);
                }
            } else {
                // 睡眠一段时间,然后重新获取内容
                // 就是以递归的方式进行循环调用
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return getCatelog2JSON();
            }
        }
        // 获取到数据,并返回
        Map<String, List<Catalog2VO>> map = JSONObject.parseObject(catelogs, new TypeReference<Map<String, List<Catalog2VO>>>() {
        });
        return map;
    }

    /**
     * 查询出所有的二级和三级分类的数据
     * 并封装为Map<String, Catalog2VO>对象
     *
     * 进行优化
     * @return
     */
    private Map<String,List<Catalog2VO>> getCatelog2JSONForDB() {
        System.out.println("为了严谨性,添加双重判断的 第二个判断");
        String catelogJson = stringRedisTemplate.opsForValue().get(CATEGORY_KEYS);
        if ("0".equals(catelogJson)) { // 查询报错结果
            return null;
        } else if (!Strings.isNullOrEmpty(catelogJson)) { // 查询正常结果
            Map<String, List<Catalog2VO>> map = JSONObject.parseObject(catelogJson, new TypeReference<Map<String, List<Catalog2VO>>>() {
            });
            return map;
        }
        System.out.println("开始查询数据库------------->");
        // 获取所有的分类的数据
        List<CategoryEntity> list = this.list();
        // 获取一级分类
        List<CategoryEntity> leve1Category = queryByParenCid(list, 0l);

        Map<String, List<Catalog2VO>> map = leve1Category.stream().collect(Collectors.toMap(
            key -> key.getCatId().toString()
            , value -> {
                // 省略具体业务
                return null;
            }
        ));
        System.out.println("防止缓存穿透与雪崩------------->");
        if (map == null || map.size() == 0) {
            // 1.添加较短时间线,是为了防止缓存穿透
            stringRedisTemplate.opsForValue().set(CATEGORY_KEYS, "0", 5, TimeUnit.SECONDS);
        } else {
            // 随机数的过期时间,是为了防止缓存雪崩
            stringRedisTemplate.opsForValue().set(CATEGORY_KEYS, JSONObject.toJSONString(map), RandomUtils.nextInt(10)+1, TimeUnit.HOURS);
        }
        return map;
    }

posted @ 2024-06-02 16:58  之士咖啡  阅读(1)  评论(0编辑  收藏  举报