目录

 一.缓存穿透

 1.缓存空对象方案实现逻辑,此处用查询店铺作为例子进行分享:

2. 实现代码:

二.缓存雪崩:

三.缓存击穿:


 一.缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

        常见的解决方案有两种:

  • 缓存空对象
  • 优点:实现简单,维护方便
  • 缺点:额外的内存消耗,可能造成短期的不一致
  • 布隆过滤器
  • 优点:内存占用较少,没有多余key
  • 缺点:实现复杂,存在误判可能

上面的方法是以被动的解决缓存穿透的问题,分享几点主动的解决方案:

  1. 增强id的复杂度,避免被猜测id的规律
  2. 做好数据的基础格式校验
  3. 加强用户权限校验
  4. 对热点参数做限流

 1.缓存空对象方案实现逻辑,此处用查询店铺作为例子进行分享:

redis缓存雪崩 缓存穿透 缓存击穿如何解决 redis缓存穿透和雪崩解决_redis

2. 实现代码:

@Override
    public Result queryShopById(Long id) {
        String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
        //1.先查询Redis缓中有没有数据
        String s = stringRedisTemplate.opsForValue().get(shopKey);
        Shop shop = new Shop();
        //Redis不为空返回
        if (StrUtil.isNotBlank(s)) {
            shop = JSONUtil.toBean(s, Shop.class);
            return Result.ok(shop);
        }
        //判断命中是否是空值
        if (s == "") {
            Result.fail("数据为空!");
        }
        //2.Redis中没有数据查询数据库中的数据
        shop = shopMapper.selectById(id);
        if (null != shop) {
            //存在写入Redis
            stringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
            return Result.ok(shop);
        }

        //缓存空值,并设置过期时间
        if (null == shop) {
            stringRedisTemplate.opsForValue().set(shopKey, "", 2L, TimeUnit.MINUTES);
        }
        return Result.fail("数据不存在");
    }

二.缓存雪崩:

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  1. 给不同的key的TTL添加随机值 (添加预热数据时随机添加超时时间)
  2. 利用Redis集群提高服务的可用性
  3. 给缓存业务添加降级限流策略
  4. 给业务添加多级缓存


三.缓存击穿:

缓存击穿问题也叫热点问题,就是一个被高并发访问并且缓存重建业务比较复杂的key突然失效,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案有两种:

  1. 互斥锁
  2. 逻辑过期

1.互斥锁和逻辑过期

redis缓存雪崩 缓存穿透 缓存击穿如何解决 redis缓存穿透和雪崩解决_Redis_02

redis缓存雪崩 缓存穿透 缓存击穿如何解决 redis缓存穿透和雪崩解决_Redis_03

 3.互斥锁和逻辑过期对比:

解决方案

优点

缺点

互斥锁

  • 没有额外的内存消耗
  • 保证一致性
  • 实现简单
  • 线程需要等待,性能受影响
  • 可能有死锁风险

逻辑过期

  • 线程无需等待,性能较好
  • 不保证一致性
  • 有额外内存消耗
  • 实现复杂

 4.基于互斥锁方式解决缓存击穿问题

redis缓存雪崩 缓存穿透 缓存击穿如何解决 redis缓存穿透和雪崩解决_数据_04

 5.代码实现:

@Override
    public Result queryShopById(Long id) {
        String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
        //1.先查询Redis缓中有没有数据
        String s = stringRedisTemplate.opsForValue().get(shopKey);
        Shop shop = new Shop();
        //Redis不为空返回
        if (StrUtil.isNotBlank(s)) {
            shop = JSONUtil.toBean(s, Shop.class);
            return Result.ok(shop);
        }
        //判断命中是否是空值
        if (s == "") {
            Result.fail("数据为空!");
        }

        String lockKey = "lock:shop" + id;
        try {
            //如果为null去获取锁
            if (!getLock(lockKey)) {
                Thread.sleep(50);  //休眠一段时间
                queryShopById(id);  //继续请求
            }
            //2.Redis中没有数据查询数据库中的数据
            shop = shopMapper.selectById(id);
            if (null != shop) {
                //存在写入Redis
                stringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
                return Result.ok(shop);
            }
            //缓存空值,并设置过期时间
            if (null == shop) {
                stringRedisTemplate.opsForValue().set(shopKey, "", 2L, TimeUnit.MINUTES);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            delLock(lockKey);
        }
        return Result.fail("数据不存在!");
    }

    //获取锁
    private Boolean getLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.MINUTES);
        return BooleanUtil.isTrue(flag);
    }

    //释放锁
    private void delLock(String key) {
        stringRedisTemplate.delete(key);
    }

6.基于逻辑过期解决缓存击穿问题(流程图)

redis缓存雪崩 缓存穿透 缓存击穿如何解决 redis缓存穿透和雪崩解决_redis_05

代码展示:

//线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    @Override  
 public Result queryShopById(Long id) {
        //验证判断是否存在缓存数据
        Result date = getDate(id);
        if (date != null) {
            return date;
        }
        //过期,尝试获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        Boolean lock = getLock(lockKey);
        //判断是否获取锁成功!
        if (lock) {
            //验证判断是否存在缓存数据
            date = getDate(id);
            if (date != null) {
                return date;
            }
            //开启独立线程,缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    //释放锁
                    delLock(lockKey);
                }
            });
            //验证判断是否存在缓存数据
            date = getDate(id);
            if (date != null) {
                return date;
            }
        }
        return Result.fail("数据为空");
    }

    private Result getDate(Long id) {
        String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
        //1.先查询Redis缓中有没有数据
        String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
        //Redis不为空返回
        if (StrUtil.isEmpty(shopJson)) {
            return Result.fail("数据为空!");
        }
        RedisData redisDataJson = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisDataJson.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisDataJson.getExpireTime();
        //判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //未过期,返回旧的店铺信息
            return Result.ok(shop);
        }
        return null;
    }