Redis缓存击穿以及解决方案

什么是Redis缓存击穿:

        也叫做热点key问题,给某一个key设置了过期时间,当key过期的时候,有大量并发请求发送过来,可能会把数据库压垮。

解决方法:

 1.设置互斥锁:

    缓存失效时,使用例如Redis的setnx设置一个互斥锁,当操作成功的时候查询数据库重建缓存。

缺点: 线程需要等待性能受到影响 ,可能会有死锁风险

黑马点评缓存商铺使用setnex案例:

如果想要测试,可以使用JMeter工具

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public Result queryById(Long id) {
        //缓存穿透 使用存null解决
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        return Result.ok(shop);
    }

    //封装缓存击穿的解决方法 使用setnx互斥锁
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1.从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否存在
        if (StrUtil.isBlank(shopJson)) {
            //3.存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //4.判断是否命中空值 为""就返回null
        if (shopJson.isEmpty()) {
            return null;
        }
        //5.实现缓存重建
        //5.1获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        Shop shop = new Shop();

        try {
            boolean isLock = tryLock(lockKey);
            //5.2判断是否获取成功
            //5.3失败,休眠并重试
            if (!isLock) {
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //5.4成功,根据id查询数据库
            shop = getById(id);
            //6.数据库中不存在 将空值存入redis 返回null
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(key, "", 2L, TimeUnit.MINUTES);
                return null;
            }
            //7.存在,写入redis 设置超时时间
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //8.释放锁
            unLock(lockKey);
        }
        //9.返回查询结果
        return shop;
    }

    //互斥锁 获取锁
    private boolean tryLock(String key) {
        //获取锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        //防止拆箱出现空指针异常
        return BooleanUtil.isTrue(flag);
    }

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

2.设置逻辑过期字段

     设置key的时候设置一个过期字段存入缓存中,查询的时候,取出字段判断是否过期,如果过期就开启一个新线程进行数据同步,当前线程正常返回数据。

缺点:不保证一致性

黑马点评缓存商铺设置逻辑过期字段案例:

 @Resource
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public Result queryById(Long id) {
        //缓存穿透 使用存null解决
        Shop shop = queryWithLogicalExpire(id);
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        return Result.ok(shop);
    }

    //线程池 用于开启独立线程
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    //封装缓存击穿的解决方法 使用逻辑过期解决缓存击穿
    public Shop queryWithLogicalExpire(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否存在 只有shopJson是非空字符串才为true
        if (StrUtil.isBlank(shopJson)) {
            //3.不存在,直接返回
            return null;
        }
        //命中 将shopJson反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //判断缓存是否过期
        if (expireTime.isAfter(LocalDateTime.now())){
            //未过期返回商铺信息
            return shop;
        }
        //过期获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        //判断是否获取成功 失败返回shop信息
        if (isLock) {
            //成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //重建缓存
                    this.saveShop2Redis(id, 20L);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }

        //返回shop信息
        return shop;
    }

    //互斥锁 获取锁
    private boolean tryLock(String key) {
        //获取锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        //防止拆箱出现空指针异常
        return BooleanUtil.isTrue(flag);
    }

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


    public void saveShop2Redis(Long id , Long expireSeconds) throws InterruptedException {
        //1.查询店铺数据
        Shop shop = getById(id);

        //模拟延迟
//        Thread.sleep(200);
        //2.封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //3.写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值