黑马点评day02-商户查询缓存

总览:

1、添加商查询redis缓存:

ShopServiceImpl.java:

public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Autowired    
    private RedisUtil redisUtil;    
    @Override   
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //        从Redis中获取数据        
        String shopJson = (String) redisUtil.get(key);
        //        判断redis是否有数据        
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);            
            return Result.ok(shop);        
            }
//        从数据库中获取数据        
        Shop shop = this.getById(id);
        //        判断数据库是否有数据        
        if (shop == null) {
            return Result.fail("店铺不存在");        }
        // 将数据存入Redis        
        redisUtil.set(key, JSONUtil.toJsonStr(shop));        
        return Result.ok(shop);    }
}

2、给店铺类型查询业务添加缓存

public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {

    @Autowired    
    private RedisUtil redisUtil;    
    @Override    
    public Result queryLsit() {
        if (redisUtil.get("typeList")!=null){
            return Result.ok(redisUtil.get("typeList"));        
            }
        LambdaQueryWrapper<ShopType> wrapper = new LambdaQueryWrapper<>();        
        wrapper.orderByAsc(ShopType::getSort);        
        List<ShopType> typeList = this.list(wrapper);        
        if (typeList == null) {
            return Result.fail("查询失败");        
            }
        redisUtil.set("typeList",typeList);        
        return Result.ok(typeList);    
        }
}

3、缓存更新策略

先操作数据库,再删除缓存发生错误概率较低,因为缓存速度快

4、实现商铺缓存与数据库相一致(商铺更新操作)

采用先操作数据库,后删除redis缓存:

public Result updateShopInfo(Shop shop) {
    Long id = shop.getId();    if (id == null) {
        return Result.fail("店铺id不能为空");    }
    String key = CACHE_SHOP_KEY + id;    
    // 更新数据库    
    this.updateById(shop);    
    // 删除Redis中的数据    
    redisUtil.del(key);    
    return Result.ok();}

5、缓存穿透解决方案(缓存空对象、布隆过滤)(通过解决查询商铺问题)

**缓存空对象思路分析:**当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了

**布隆过滤:**布隆过滤器
其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,

1、采用缓存空对象

修改shopserviceImpl.java:

public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Autowired    
    private RedisUtil redisUtil;    
    @Override    
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
//        从Redis中获取数据        
    String shopJson = (String) redisUtil.get(key);
//        判断redis是否有数据        
    if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);            
            return Result.ok(shop);        
            }
//       缓存穿透 判断是否是空对象        
        if (shopJson!=null){
            return Result.fail("店铺不存在2");        
            }
//        从数据库中获取数据        
        Shop shop = this.getById(id);
//        判断数据库是否有数据        
        if (shop == null) {
//            利用缓存空对象,防止缓存穿透            
        redisUtil.set(key,"",60*2);            
        return Result.fail("店铺不存在");       
 }
// 将数据存入Redis        
        redisUtil.set(key, JSONUtil.toJsonStr(shop),60*30);        
        return Result.ok(shop);    
        }

总结:什么叫缓存穿透&&解决措施

缓存雪崩

提高redis高可用:使用redis集群,借助redis哨兵机制

缓存击穿:

解决方案一、使用锁来解决:

因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。
解决方案二、逻辑过期方案

方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。

我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。

这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。
进行对比
都是解决缓存重建周期内并发的问题

**互斥锁方案:(保证一致性)**由于保证了互斥性(线程等待),所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响

**逻辑过期方案:(保证可用性)** 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦(需要维护逻辑过期时间)

7、基于互斥锁解决缓存击穿问题:(查询商铺问题)

采用自定义互斥锁:

本项目采用redis实现 : setnx... del...

封装之前缓存击穿的解决方法函数:

/**     
* 缓存穿透解决函数     
* @param id     
* @return     
*/    
public Shop dealCachePass(Long id) {
        String key = CACHE_SHOP_KEY + id;
//        从Redis中获取数据        
        String shopJson = (String) redisUtil.get(key);
//        判断redis是否有数据        
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);            
            return shop;        
            }
//       缓存穿透 判断是否是空对象        
        if (shopJson != null) {
            return null;        
            }
//        从数据库中获取数据        
        Shop shop = this.getById(id);
//        判断数据库是否有数据        
        if (shop == null) {
//            利用缓存空对象,防止缓存穿透            
        redisUtil.set(key, "", 60 * 2);            
        return null;        
}
        // 将数据存入Redis        
        redisUtil.set(key, JSONUtil.toJsonStr(shop), 60 * 30);        
        return shop;    
        }

封装取得互斥锁和释放互斥锁方法:

/** 
* 获取互斥锁 
* @param lockKey 
* @return 
*/
public boolean getMutexLock(String lockKey) {
    Boolean flag = redisUtil.setnx(lockKey, "1", 10, TimeUnit.SECONDS);    
    return BooleanUtil.isTrue(flag);
    }

/** 
* 释放互斥锁 
* @param lockKey 
*/
public void unlockMutexLock(String lockKey) {
    redisUtil.del(lockKey);
    }

创建缓存击穿解决函数:

/**     
* 缓存击穿解决函数     
* @param id     
* @return     
*/    
public Shop dealCacheBreak(Long id) {
        String key = CACHE_SHOP_KEY + id;
//        从Redis中获取数据        
        String shopJson = (String) redisUtil.get(key);
//        判断redis是否有数据        
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);            
            return shop;        
            }
//       缓存穿透 判断是否是空对象        
        if (shopJson != null) {
            return null;        
            }
//         4.缓存重建
//        4.1 获取互斥锁        
        String lockKey = LOCK_SHOP_KEY + id;        
        Shop shop = null;        
        try {
            boolean mutexLock = getMutexLock(lockKey);
//        4.2 判断是否获取到锁
//        获取不到锁,等待50ms,再次获取            
            if (!mutexLock) {
                Thread.sleep(50);                
//            递归调用自身                
                return dealCacheBreak(id);            
                }
//        4.3 获取到锁,再次检测redis缓存是否存在,做DoubleCheck检查redis缓存            
//        判断redis是否有数据            
        if (StrUtil.isNotBlank(shopJson)) {
                shop = JSONUtil.toBean(shopJson, Shop.class);                
//        释放锁                
                unlockMutexLock(lockKey);                
                return shop;            
                }
//        再从数据库中获取数据            
        shop = this.getById(id);
//            模拟线上查库重建缓存延迟            
        Thread.sleep(200);
//        判断数据库是否有数据            
        if (shop == null) {
//        利用缓存空对象,防止缓存穿透                
        redisUtil.set(key, "", 60 * 2);                
        return null;            
}
//        将数据存入Redis            
        redisUtil.set(key, JSONUtil.toJsonStr(shop), 60 * 30);        
        } catch (InterruptedException e) {
            throw new RuntimeException(e);        
            } finally {
//        4.4 释放锁            
        unlockMutexLock(lockKey);        
        }
        return shop;    
       }

修改查询商铺queryById():


//        解决缓存击穿        
        Shop shop = dealCacheBreak(id);        
        if (shop == null) {
            return Result.fail("商铺不存在");        
            }
        return Result.ok(shop);    
        }

验证:

使用Apache JMeter:

参照全网最全最细的jmeter接口测试教程以及接口测试流程详解 - 知乎 (zhihu.com)

创建线程组,指定线程数和Ramp-Up,表示在5s内启动1000个线程:

创建Http请求并配置:

查看结果:

1000个请求都已成功返回信息

Ide控制台也只显示一条sql,表明仅有一个线程会查库,其余线程在互斥锁取得后进行doublecheck时拿到了Redis中的缓存

Redis也已上库:

至此,热点key问题的互斥锁(使用redis-string-setnx方式)解决方式完成

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

编写savetoRedis方法:

实现热点key初始化和后续再刷新redis数据方法:

//    保存到redis缓存(适用缓存击穿逻辑过期方式)    
public void savetoRedis(Long id, Long seconds) throws InterruptedException {
//        查询商铺数据        
Shop shop = this.getById(id);        
Thread.sleep(200);
//        逻辑封装        
RedisData redisData = new RedisData();        
redisData.setData(shop);        
redisData.setExpireTime(LocalDateTime.now().plusSeconds(seconds));        
String key = CACHE_SHOP_KEY + id;
//        写入Redis中        
redisUtil.set(key, JSONUtil.toJsonStr(redisData));    
}
}

编写完后直接在test方调用savetoRedis(),实现redis初始化。

编写dealCahceBerak2方法,实现逻辑过期时间方式处理缓存击穿:

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

注意:仍需要使用DoubleCheck在获取到互斥锁之后判断缓存时间是否过期,避免由于使用多线程造成重复rebuild cache

public Shop dealCacheBreak2(Long id) {
        String key = CACHE_SHOP_KEY + id;
//        从Redis中获取数据        
        String shopJson = (String) redisUtil.get(key);
//        判断redis有无数据
//        redis无数据,返回空对象        
        if (StrUtil.isBlank(shopJson)) {
            return null;        
            }
//       redis有数据,判断逻辑时间是否过期        
        RedisData redisDate = JSONUtil.toBean(shopJson, RedisData.class);        
        Shop shop = JSONUtil.toBean((JSONObject) redisDate.getData(), Shop.class);        
        LocalDateTime expireTime = redisDate.getExpireTime();
//       判断时间是否过期        
        if (expireTime.isAfter(LocalDateTime.now())) {
//       时间未过期            
        return shop;        
        }
//       时间已过期,异步重建缓存
//       获取互斥锁        
        String lockKey = LOCK_SHOP_KEY + id;        
        boolean mutexLock = getMutexLock(lockKey);
//      获取到锁成功,开启异步线程重建缓存        
        if (mutexLock) {
//       Doublecheck检查redis是否过期
//       判断时间是否过期            
        if (expireTime.isAfter(LocalDateTime.now())) {
//       时间未过期                
        return shop;            
}
//       开启独立线程(使用线程池)            
        CACHE_REBUILD_EXECUTOR.submit(() -> {
       try {
               this.savetoRedis(id, 20L);                
            } catch (Exception e) {
            throw new RuntimeException(e);                
            } finally {
//       释放锁                    
            unlockMutexLock(lockKey);                
            }
          });        
        }
//      获取锁失败        
        return shop;    
}

改写queryById方法:

 public Result queryById(Long id) {
//        解决缓存穿透
//        Shop shop = dealCachePass(id);
//        解决缓存击穿-互斥锁方式
//        Shop shop = dealCacheBreak(id);
//        解决缓存击穿-逻辑过期时间方式        
        Shop shop = dealCacheBreak2(id);        
        if (shop == null) {
            return Result.fail("商铺不存在");        
            }
        return Result.ok(shop);    
        }

验证:

我们已经调用了savetoRedis()实现了redis初始化操作

name:1085-Tea

修改数据库数据,验证一致性以及多线程并发访问:

name:8080-Tea

JMter设值100线程在一秒内执行完:

执行查询,观察到:

第一个线程返回初始化数据,name:1085-Tea,

此时由于获取到互斥锁,且逻辑时间过期,开始查库话费200ms,故往后大概20个线程(每个线程花费10ms)均返回name:1085-Tea

直到大概22个线程返回:name:8080-Tea

Ide仅执行一次sql:

至此,验证完毕,实现了两库数据一致性和多线程并发访问仅执行一次sql查询,完成热点Key问题逻辑过期时间解决方式

9、封装stringRedisTemplate方法实现缓存工具类:

StringRedisUtil,java:

@Component
@SuppressWarnings("all")
public class StringRedisUtil {
    @Autowired    
    public StringRedisTemplate stringRedisTemplate;    
    @Autowired    public RedisUtil redisUtil;    
    //* 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间    
    public void set(String key, Object value, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);    
        }


    //* 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题    
    public void setWithLogicExpire(String key, Object value, Long time, TimeUnit timeUnit) {
//        使用RedisData类来封装value和time        
RedisData redisData = new RedisData();        
redisData.setData(value);        
redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
//        写入redis        redisUtil.set(key, JSONUtil.toJsonStr(redisData));    
}

//* 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题    
/**     
* 缓存穿透解决函数(缓存空对象)     
* @param prefix     
* @param id     
* @param type     
* @param function     
* @param time     
* @param timeUnit     
* @param <R>     
* @param <ID>     
* @return     
*/        
public <R, ID> R dealCachePass(String prefix, ID id, Class<R> type, Function<ID, R> function, Long time, TimeUnit timeUnit) {
        String key = prefix + id;
        //        从Redis中获取数据        
        String json = stringRedisTemplate.opsForValue().get(key);
        //        判断redis是否有数据        
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);        
            }
//       缓存穿透 判断是否是空值        
        if (json != null) {
            return null;        
            }
//        从数据库中获取数据        
        R r = function.apply(id);
//        判断数据库是否有数据        
        if (r == null) {
//            利用缓存空对象,防止缓存穿透            
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);            
        return null;        
}
        // 将数据存入Redis        
        this.set(key, r, time, timeUnit);        
        return r;    
        }
    
    //* 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题    
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);    
    public <R, ID> R dealCacheBreakLogic(String prefix, ID id, Class<R> type, Function<ID, R> function, Long time, TimeUnit timeUnit) {
        String key = prefix + id;
        //        从Redis中获取数据        
        String json = (String) redisUtil.get(key);
        //        判断redis有无数据
        //        redis无数据,返回空对象        
        if (StrUtil.isBlank(json)) {
            return null;        
            }
//       redis有数据,判断逻辑时间是否过期        
        RedisData redisDate = JSONUtil.toBean(json, RedisData.class);        
        R r = JSONUtil.toBean((JSONObject) redisDate.getData(), type);        
        LocalDateTime expireTime = redisDate.getExpireTime();
//       判断时间是否过期        
        if (expireTime.isAfter(LocalDateTime.now())) {
//       时间未过期            
        return r;       
 }
//       时间已过期,异步重建缓存
//       获取互斥锁        
        String lockKey = LOCK_SHOP_KEY + id;        
        boolean mutexLock = getMutexLock(lockKey);
//      获取到锁成功,开启异步线程重建缓存        
        if (mutexLock) {
//       Doublecheck检查redis是否过期
//       判断时间是否过期            
        if (expireTime.isAfter(LocalDateTime.now())) {
//       时间未过期                
        return r;            
        }
//       开启独立线程(使用线程池)            
        CACHE_REBUILD_EXECUTOR.submit(() -> {
          try {
//          从数据库中获取数据                    
        R newr=function.apply(id);                    
        this.setWithLogicExpire(key,newr,time,timeUnit);                
        } catch (Exception e) {
           throw new RuntimeException(e);                
            } finally {
//    释放锁                    
        unlockMutexLock(lockKey);                
        }
            });        
            }
//   获取锁失败        
    return r;    
}

    /**     
    * 获取互斥锁     
    *     
    * @param lockKey     
    * @return     
    */    
    public boolean getMutexLock(String lockKey) {
        Boolean flag = redisUtil.setnx(lockKey, "1", 10, TimeUnit.SECONDS);        
         BooleanUtil.isTrue(flag);    
         }
    /**     
    * 释放互斥锁     
    *     
    * @param lockKey     
    */    
    public void unlockMutexLock(String lockKey) {
        redisUtil.del(lockKey);    }
}

难点在于封装时泛型以及具体形参的选择

本工具另外使用了RedisUtil类,可以参照如下:(26条消息) 【免费】RedisUtil方法封装类和RedisConfig配置类资源-CSDN文库

验证方法同上文一致。

总结:

**内存淘汰:**redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)

**超时剔除:**当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存

**主动更新:**我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题

  • 0
    点赞
  • 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、付费专栏及课程。

余额充值