黑马点评第二个模块---商户查询缓存

什么是缓存

缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。

缓存的作用:

  • 降低后端负载
  • 提高读写效率,降低响应时间

缓存的成本:

  • 数据一致性成本
  • 代码维护成本
  • 运维成本

添加Redis缓存

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-40ZOVjvt-1668333180853)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221110222210784.png)]

商铺查询缓存代码实现

 @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Object queryById(Long id) {
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //4.不存在,返回id查询数据库
        Shop shop = getById(id);
        //5.不存在,返回错误
        if (shop==null){
            return Result.fail("店铺不存在! ");
        }
        //6.存在,写入redis
        String jsonShop = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonShop);
        //7.返回
        return  Result.ok(shop);
    }

店铺类型查询添加缓存:

  @Override
    public List<ShopType> getTypeList() {

        //先查询redis
        String shopType = stringRedisTemplate.opsForValue().get(CACHE_TYPE_KEY);
        //如果有数据就返回
        if (shopType!=null){
                return JSONUtil.toList(shopType,ShopType.class);
        }
        //如果没有数据就查询数据库
        List<ShopType> shopTypes = this.list();
        String toJsonStr = JSONUtil.toJsonStr(shopTypes);
        //将查询的数据写入到redis中
        stringRedisTemplate.opsForValue().set(CACHE_TYPE_KEY,toJsonStr);
        return shopTypes;
    }

缓存更新策略

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ojrcoPDr-1668333180855)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111145318590.png)]

业务场景:

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

主动更新策略:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VLIAN4aH-1668333180856)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111150210637.png)]

第一种方案需要考虑的问题:

  1. 删除缓存还是更新缓存?
    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多 ×
    • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存
  2. 如何保证缓存与数据库的操作的同时成功或失败?
    • 单体系统,将缓存与数据库操作放在一个事务
    • 分布式系统,利用TCC等分布式事务方案
  3. 先操作缓存还是先操作数据库?
    • 先删除缓存,在操作数据库
    • 先操作数据库,再删除缓存

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w8PZffss-1668333180857)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111151640729.png)]

线程1删除缓存之后要更新数据库,同时线程2查询缓存未命中,然后查询数据库,写入缓存。

在这个过程中由于更新数据过程慢而查询的速度快,线程2查出的缓存依然是旧缓存。

从而造成数据库与缓存数据不一致问题。

上图发生线程安全问题的条件:

当一个线程更新数据库时删除缓存时,另一个线程进行查询操作(发生概率大)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x1sozMrD-1668333180857)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111152126682.png)]

线程1查询缓存未命中,查询数据库之后要写入缓存时,有一个线程2插入进来更新数据库,由于缓存本来就没有所以删除缓存也没有。当线程2执行完之后,线程1将查询的旧数据写入缓存中(小概率)

上图发生线程安全问题的条件:

2个线程并行执行。

缓存刚好失效。

并且线程2要在线程1写缓存这个微秒中完成。

综上:

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

  1. 低一致性需求:使用Redis自带的内存淘汰机制
  2. 高一致性需求:主动更新,并超时剔除作为兜底方案
    • 读操作:
      • 缓存命中直接返回
      • 缓存未命中则查询数据库,并写入缓存,设定超时时间
    • 写操作:
      • 先写数据库,然后再删除缓存
      • 要确保数据库与缓存操作的原子性

给查询商铺的缓存添加超时剔除和主动更新的策略

修改ShopController中的业务逻辑,满足下面的需求:

  1. 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
  2. 根据id修改店铺时,先修改数据库,再删除缓存

实现商铺缓存与数据库的双写一致性问题(即在更新数据库时,使缓存更新)

代码实现

@Transactional
    public Result update(Shop shop) {
        Long id =shop.getId();
        if (id ==null){
            return Result.fail("修改失败");
        }
        //1.更新数据库
         this.updateById(shop);
         //2.删除缓存
         stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
         return Result.ok("修改成功");
    }

缓存穿透

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

常见的解决方案有两种:

  • 缓存空对象
    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UGy8hK1V-1668333180859)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111231851264.png)]

    • 优点:实现简单,维护方便
    • 缺点:
      • 额外的内存消耗
      • 可能造成短期的不一致
  • 布隆过滤器
    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-37dZFTJT-1668333180860)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111231936528.png)]

    • 原理:将数据库中的数据对象通过hash算法计算出哈希值,将哈希值转化二进制位保存在布隆过滤器中。去判断数据是否存在,就是判断位置是0还是1,以此去判断数据是否存在。(概率上的统计不是百分之百,不存在是肯定的,存在是不一定的)
  • 优点:内存占用较小,没有多余key

  • 缺点:

    • 实现复杂
    • 存在误判可能

解决商铺查询的缓存穿透问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2kipk3e9-1668333180860)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111233239817.png)]

代码改变ShopServiceImpl

     //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //判断命中的是否是空值
        if (shopJson!=null){
            return Result.fail("店铺不存在! ");
        }
        //4.不存在,返回id查询数据库
        Shop shop = getById(id);
        //5.不存在,返回错误
        if (shop==null){
        //解决缓存穿透,将空值写入redis
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return Result.fail("店铺不存在! ");
        }
        //6.存在,写入redis
        String jsonShop = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonShop,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //7.返回
        return  Result.ok(shop);     

总结:

缓存穿透产生的原因是什么?

  • 用户请求的数据在缓存中和数据库都不存在,不断发起这样的请求,给数据库带来巨大压力

缓存穿透的解决方案有哪些?

  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限量

缓存雪崩

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aJ1td6G1-1668333180862)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221112141444258.png)]

解决方案:

  • 给不同的key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JScjTItm-1668333180863)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221112142801953.png)]

常见的解决方案有两种:

  • 互斥锁
    • 优点
      • 没有额外的内存消耗
      • 保证一致性
      • 实现简单
    • 缺点
      • 线程需要等待,性能受影响
      • 可能有死锁风险
  • 逻辑过期
    • 优点
      • 线程无需等待,性能较好
    • 缺点
      • 不保证一致性
      • 有额外内存消耗
      • 实现复杂

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xOONmy86-1668333180864)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221112143709358.png)]

基于互斥锁解决缓存击穿问题

需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4dCgyfo-1668333180869)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221112150246274.png)]

代码实现

//缓存击穿互斥锁实现
    public Shop queryWithMutex(Long id) {
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //判断命中的是否是空值
        if (shopJson != null) {
            return null;
        }

        //4.实现缓存重建
        //4.1获取互斥锁
        Shop shop;
        try {
            boolean tryLocal = tryLocal(LOCK_SHOP_KEY + id);
            //4.2判断是否获取成功
            if (!tryLocal) {
                //4.3失败,则休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }

            //注意:获取锁成功应该再次检测redis缓存是否存在,做两次检查,如果存在则无需重建缓存
            String shopJson2 = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
            //2.判断是否存在
            if (StrUtil.isNotBlank(shopJson2)) {
                //3.存在,直接返回
                return JSONUtil.toBean(shopJson2, Shop.class);
            }

            //4.4成功,根据id查询数据库
            shop = getById(id);
           /* //模拟重建的延迟
            Thread.sleep(200);*/
            //5.不存在,返回错误
            if (shop == null) {
                return null;
            }
            //6.存在,写入redis
            String jsonShop = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonShop, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
           throw  new RuntimeException(e);
        }finally {
            //7.释放互斥锁
            unLock(LOCK_SHOP_KEY + id);
        }
        //8.返回
        return shop;
    }

基于逻辑过期方式解决缓存击穿问题

需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-soMxfvTD-1668333180870)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221112214049954.png)]

代码实现

//缓存击穿的逻辑过期代码实现
    public Shop queryWithLogicalExpire(Long id) {
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        //2.判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            //3.不存在,直接返回
            return null;
        }
        //4.命中,需要先把json反序列化为对象
        RedisData<Shop> redisData = JSON.parseObject(shopJson,new TypeReference<RedisData<Shop>>(){});
        Shop shop=redisData.getData();
        LocalDateTime expireTime = redisData.getExpireTime();
        //5.判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())){
            //5.1未过期,直接返回店铺信息
            return shop;
        }

        //5.2已过期,需要缓存重建

        //6.缓存重建

        //6.1判断是否获取锁成功
        if (tryLocal(LOCK_SHOP_KEY+id)) {
            //6.2成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存
                    this.saveShopToRedis(id,20L);
                } catch (Exception e) {
                        throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(LOCK_SHOP_KEY+id);
                }
            });
            return shop;
        }
        //6.3返回过期的商铺信息
        return null;
    }
    public void saveShopToRedis(Long id,Long expireSeconds) throws InterruptedException {
        //1.查询店铺数据
        Shop shop=getById(id);
        Thread.sleep(200);
        //2.封装逻辑过期时间
        RedisData<Shop> redisData = new RedisData<>();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //3.写入Redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }

缓存工具封装:

基于StringRedisTemplate封装一个缓存工具类,满足下列需求:

方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间

方法2:将任意Java对象序列化为json并存储在string类型的key中,并且key设置逻辑过期时间,用于处理缓存击穿问题

方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题

方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

代码实现

@Slf4j
@Component
public class CacheClient {
    private StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(value),time,unit);
    }

    /**
     *将任意Java对象序列化为json并存储在string类型的key中,并且key设置逻辑过期时间,用于处理缓存击穿问题
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void setWithLogicalExpire(String key,Object value,Long time,TimeUnit unit){

        //设置逻辑过期
        RedisData<Object> redisData =new RedisData<>();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        //写入redis
        stringRedisTemplate.opsForValue().set(key,JSON.toJSONString(redisData));
    }

    /**
     *
     * @param keyPrefix key的前缀
     * @param id
     * @param type   具体的类型
     * @param <R>     泛型  返回值
     * @param <ID>    泛型 id
     * @return
     */
    //实现缓存穿透的工具
    public <R,ID> R   queryWithPassThrough(String keyPrefix, ID id, Class<R> type,
                                           Function<ID,R> dbFallback,Long time,TimeUnit unit) {
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);

        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            return JSON.parseObject(shopJson, type);
        }
        //判断命中的是否是空值
        if (shopJson != null) {
            return null;
        }
        //4.不存在,返回id查询数据库
        R shop = dbFallback.apply(id);
        //5.不存在,返回错误
        if (shop == null) {
            //解决缓存穿透,将空值写入redis
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //6.存在,写入redis
        set(CACHE_SHOP_KEY + id,shop,time,unit);
        //7.返回
        return shop;
    }

    //定义一把锁
    private boolean tryLocal(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "Lock", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //释放锁
    private void unLock(String key) {
        Boolean flag = stringRedisTemplate.delete(key);
    }
    //自定义线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    //实现缓存击穿的逻辑过期工具
    public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,
                                           Function<ID,R> dbFallBack,Long time,TimeUnit unit) {
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        //2.判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            //3.不存在,直接返回
            return null;
        }
        //4.命中,需要先把json反序列化为对象
        RedisData<R> redisData = JSON.parseObject(shopJson,new TypeReference<RedisData<R>>(){});
        R shop= redisData.getData();
        LocalDateTime expireTime = redisData.getExpireTime();
        //5.判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())){
            //5.1未过期,直接返回店铺信息
            return shop;
        }

        //5.2已过期,需要缓存重建

        //6.缓存重建

        //6.1判断是否获取锁成功
        if (tryLocal(LOCK_SHOP_KEY+id)) {

            //1.从redis查询商铺缓存
            String shopJson2 = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

            //2.判断是否存在
            if (StrUtil.isBlank(shopJson2)) {
                //3.不存在,直接返回
                return null;
            }
            //4.命中,需要先把json反序列化为对象
            RedisData<R> redisData2 = JSON.parseObject(shopJson,new TypeReference<RedisData<R>>(){});
            R shop2= redisData.getData();
            LocalDateTime expireTime2 = redisData2.getExpireTime();
            //5.判断是否过期
            if (expireTime2.isAfter(LocalDateTime.now())){
                //5.1未过期,直接返回店铺信息
                return shop2;
            }
            //6.2成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存
                    R r1=dbFallBack.apply(id);
                    Thread.sleep(200);
                    //写入redis
                    setWithLogicalExpire(keyPrefix+id,r1,time,unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(LOCK_SHOP_KEY+id);
                }
            });
        }
        //6.3返回过期的商铺信息
        return shop;
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 黑马qt公开课是一套为期5天的课件,主要介绍了Qt框架的基础知识和应用。Qt框架是一个跨平台的开发工具,可以方便地进行应用程序的设计、开发和调试,被广泛应用于图形界面开发、嵌入式系统和移动应用等领域。 在5天的课程中,学习者将对Qt框架的整体架构有一个全面的认识,包括Qt及其常用库的概念、功能和用法,也学会了如何使用Qt Designer进行界面设计和基于信号与槽的事件编程。 此外,课程还将介绍Qt中常用的编程模式和技术,如MVC架构、文件操作、网络编程等,并通过实例让学习者深入理解和应用这些概念和技术。 5天的课件中还提供了大量的实践操作,让学习者通过编写实际案例,深入理解所学知识,并更好地掌握Qt框架的基础和应用,为以后的工作打下坚实的基础。 总之,如果你想快速入门Qt框架的基础知识和应用,那么黑马qt公开课—5天的课件,将是一个非常好的选择。 ### 回答2: 黑马qt公开课的课件共分为5天,内容涵盖了Qt的基础知识、UI设计、绘图系统、多线程编程和网络编程等方面。通过这5天的学习,学员可以全面掌握Qt的开发技能和应用场景,具备开发Qt应用程序的能力。 第一天课程主要介绍了Qt的基础知识,包括Qt窗口和控件、信号与槽机制、事件处理、布局和样式等内容。通过这些基础知识的学习,学员可以了解Qt的基本工作原理和操作方法。 第二天的课程主要讲解了Qt的UI设计,包括UI设计器的使用、自定义控件和样式等内容。学员可以从中学习到如何设计美观、直观的用户界面。 第三天的课程则主题为Qt的绘图系统,包括2D和3D绘图、动画效果和图形转换等内容。在这一天的课程中,学员可以学习到如何使用Qt进行图形绘制和界面效果的优化。 第四天的课程主要介绍了Qt的多线程编程,包括线程的创建和管理、互斥锁和信号量等内容。学员可以从中学习到如何在Qt中实现多线程应用程序。 第五天的课程则主题为Qt的网络编程,包括socket编程、HTTP协议和Web服务等内容。学员可以从中学习到如何使用Qt进行网络编程,实现客户端和服务器的互通。 总体来说,黑马qt公开课的5天课程涵盖了Qt的核心知识点,让学员能够全面掌握Qt的开发技能和应用场景。通过这些课程的学习,学员可以成为一名合格的Qt开发工程师。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值