【redis入门到实战 day4】

6 篇文章 0 订阅
3 篇文章 0 订阅

缓存更新策略及穿透,雪崩,击穿的解决方案


什么是缓存,缓存的概念

缓存就是数据交换的缓冲区(称作Cache [ kæʃ ] ),是存贮数据的临时地方,一般读写性能较高。
在这里插入图片描述
在这里插入图片描述

缓存的更新策略

业务场景: 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
总结

  • 缓存更新策略的最佳实践方案:

  • 低一致性需求:使用Redis自带的内存淘汰机制

  • 高一致性需求:主动更新,并以超时剔除作为兜底方案

  • 读操作: 缓存命中则直接返回 缓存未命中则查询数据库,并写入缓存,设定超时时间

  • 写操作: 先写数据库,然后再删除缓存 要确保数据库与缓存操作的原子性

代码示例-主动更新策略


/**
     * 事务控制双写
     * @param shop
     * @return
     */
    @Override
    @Transactional
    public Result updateByShopId(Shop shop) {
        if(shop.getId() == null){
            return  Result.fail("店铺不存在");

        }
        //更新数据库
        updateById(shop);
        //删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY+shop.getId());
        return Result.ok();
    }

缓存穿透

在这里插入图片描述
在这里插入图片描述

代码实现缓存穿透解决方案

 private Shop queryWithPassThrough(Long id){
        //去redis中查询
        String key = CACHE_SHOP_KEY+id;
        String shopCache = stringRedisTemplate.opsForValue().get(key);
        Shop shop = new Shop();
        //如果不为空则直接返回
        if(StrUtil.isNotBlank(shopCache)){
            shop = JSONUtil.toBean(shopCache, Shop.class);
            return shop;
        }
        //判断命中的是否是空值
        if(shopCache!=null){
            return null;
        }

        //为空则去数据库查询
        shop= getById(id);
        //若数据库也没查到,redis中存放key值,避免缓存穿透问题
        if(shop == null){
            //放入空值
            stringRedisTemplate.opsForValue().set(key,"");
            //设置过期时间
            stringRedisTemplate.expire(key, Duration.ofMinutes(CACHE_NULL_TTL));
            return null;
        }
        //如果查到了则放入缓存中
        String shopStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key,shopStr);
        return shop;

    }

缓存穿透产生的原因是什么?
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
缓存穿透的解决方案有哪些?
缓存null值
布隆过滤
增强id的复杂度,避免被猜测id规律
做好数据的基础格式校验
加强用户权限校验
做好热点参数的限流

缓存雪崩

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

在这里插入图片描述
第一个解决方案给不同 key值的过期时间加个随机值即可

注:涉及分布式,后期学了再更新

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
两种解决方案: 1-互斥锁 2-逻辑过期
在这里插入图片描述

互斥锁与逻辑过期

在这里插入图片描述
在这里插入图片描述

互斥锁解决方案代码案例

在这里插入图片描述

    private Shop queryWithMutex(Long id){
        //1-去redis中查询商铺信息
        String key = CACHE_SHOP_KEY+id;
        String shopCache = stringRedisTemplate.opsForValue().get(key);
        Shop shop = new Shop();
        //2-如果不为空则直接返回
        if(StrUtil.isNotBlank(shopCache)){
            shop = JSONUtil.toBean(shopCache, Shop.class);
            return shop;
        }
        
        //3-判断命中的是否是空格即 空格
        if(shopCache != null){
            //是空值 则直接拦截
            return null;
        }
        //4 -不是空值- 需要缓存重建
        //5-缓存重建
        //5.1-获取互斥锁
        String lockKey =LOCK_SHOP_KEY + id;
        try {
            boolean flag = tryLock(lockKey);
            //5.2- 判断是否获取成功
            if(!flag){
                //5.3 - 失败则休眠并重试
                Thread.sleep(50);
                return  queryWithMutex(id);
            }
            //5.4 - 成功 查询数据库写入缓存
            shop= getById(id);
            //模拟延迟场景  延长重建时间
            Thread.sleep(200);
            //5.5 - 若数据库也没查到,redis中存放key值及空值,避免缓存穿透问题
            if(shop == null){
                //放入空值
                stringRedisTemplate.opsForValue().set(key,"");
                //设置过期时间
                stringRedisTemplate.expire(key, Duration.ofMinutes(CACHE_NULL_TTL));
                return null;
            }
            //5.5 - 如果查到了则放入缓存中
            String shopStr = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(key,shopStr);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放互斥锁
            unlock(lockKey);
        }
        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);
    }

逻辑过期方案

在这里插入图片描述
逻辑过期需要用到线程,这里用线程池创建

 private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

需要先往redis中放入一个热点key 设置好逻辑过期时间,用来模拟

  public void saveShop2Redis(Long id ,Long expireSeconds) throws InterruptedException {
        //查询店铺数据
        Shop shop = getById(id);
        Thread.sleep(200);
        //封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }

//单元测试代码
    @Resource
    ShopServiceImpl shopService;
    @Test
    void saveShop2Redis() throws InterruptedException {
        shopService.saveShop2Redis(1L,10L);
    }

代码实现-逻辑过期的方式解决缓存击穿

   private Shop queryWithLogicExpire(Long id){
        //1-去redis中查询
        String key = CACHE_SHOP_KEY+id;
        String shopCache = stringRedisTemplate.opsForValue().get(key);
        //2 如果未命中 则直接返回
        if(StrUtil.isBlank(shopCache)){
            return null;
        }
        //3 命中,先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopCache, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        //获取过期时间
        LocalDateTime expireTime = redisData.getExpireTime();
        //4 判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //未过期 直接返回店铺信息
            return shop;
        }
        //5 已过期 需要缓存重建
        //5.1 获取互斥锁
        String lockKey = LOCK_SHOP_KEY+id;
        boolean isLock = tryLock(lockKey);
        //5.2 判断是否获取成功
        if(isLock){
            //5.3 成功 开启独立线程 实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                //5.4 缓存重建
                try {
                    saveShop2Redis(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unlock(lockKey);
                }

            });
        }

        //5.5 返回过期对象
        return shop;

    }
    @Override
    public Result queryShopById(Long id) {
        //缓存穿透
//         Shop shop = queryWithPassThrough(id);

        //缓存击穿  互斥锁
//        Shop shop = queryWithMutex(id);

        //逻辑过期来解决缓存击穿
        Shop shop = queryWithLogicExpire(id);

        if(shop == null){
            return Result.fail("店铺信息不存在");
        }
        return Result.ok(shop);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值