黑马点评项目学习笔记

发布时间 2023-03-22 21:13:24作者: better_tomorrow

黑马点评项目

  • image-20221118194722356
  • 运行前端代码,将nginx打包的前端代码直接放到自己的工作空间中,然后再当前地址打开cmd 控制台,然后输入start nginx.exe运行这个文件,前端代码就可以访问了
    • 然后直接到浏览器中通过http://localhost:8080/访问前端页面

短信登录功能

发送验证码功能

  • 就是点击发送验证码后调用的是sendCode的controller,然后这个方法调用service中的sendCode方法,并将传过来的参数phone和Session传递过去
  • 在service的sendCode中首先校验传过来的手机号格式是否正确,正确的话随机生成一个验证码,将验证码保存到session中,(这里有一个注意点,每一个独立的浏览器在发请求的时候都有一个独立的session空间,也就是不同的浏览器访问服务器通过code 获得的值是自己对应的值,并不会串用,实际上创建session的时候tomcat就会自动生成sessionid,写到用户浏览器的cookie中,浏览器每次请求的时候都会携带这些cookie,服务器检查cookie后会将这个http请求放到对应的session域中)并打印日志,返回ok
  • 使用的正则表达式这样的固定字符串同一存储到了一个叫RegexPatterns的抽象类中(里边只有一些static final的方法)

用到的小功能

  • 生成验证码使用的是RandomUtil中的randomNumbers(随机生成数的位数)(RandomUtil是导入hutool依赖可以用的)

    •     <groupId>cn.hutool</groupId>
          <artifactId>hutool-all</artifactId>
          <version>5.7.17</version>
      
  • hutool中还有RandomUtil.randomString(int)方法用于生成固定长度的随机字符串,这里用来设置用户名

  • StrUtil.isBlank(str)也是hutool中的一个小工具可以用来检测传过来的字符串是否为空白字符(null,长度为0,里边全是空格)

  • str.matches(正则表达式字符串)这个matches是String中的方法,用来校验str字符串是否符合这个正则表达式

  • 返回的结果是统一用一个叫Result的类封装的,里边有ok(),ok(Object data),ok(List<?> data,Long total)和fail(String errorMsg)这些方法都是静态方法,返回的都是这个类对象,这些静态方法的参数都对应的有这个类的成员变量,会将参数的内容存到成员变量中去

登陆功能

  • 首先在utils包下创建了LoginInterceptor类,然后在这个类中重写了preHandle方法和afterCompletion方法,第一个是当用户没有登陆时(也就是在Session中没有查到对应的用户信息request.getSession().getAttribute("user")?,返回专用的码给前端做请求转发,跳转到登陆页面,如果查到对应的用户信息,就将这个用户信息存储到ThreadLocal中(自己将ThreadLocal封装为了UserHolder类,并将其设置为一个单例类,设置保存,获取,删除,存放方法,在ThreadLocal中存的UserDto,此处的userDto是user的阉割版,因为user中存储了从数据库中查到的所有信息,如果这些都存储到服务器上,不仅会增加服务器的压力,后续在使用的时候还会出现安全性问题(用户信息泄露)),到此,用户登陆中的拦截器以及数据保存写好了
  • 然后到config包中的MvcConfig配置类(配置类要加@Configuration)中加载这个拦截器,具体的方法就是重写addInterceptors,然后registry.addInterceptor(new 自己写的拦截器对象).excludePathPatterns("放行的url","放行的url"...);
  • 然后就是实现对应的业务功能了,就是在userController中调用service的login方法(用户传过来的信息,session),然后登陆的时候先校验手机号格式,然后校验验证码,然后调用dao层的对象查询用户的信息,如果结果为空,就新建这个用户的信息,相当于新用户注册,然后将用户的信息保存到session中,供拦截器校验

session共享数据

  • 多台Tomcat并不共享session存储空间,当请求切换到不同的Tomcat服务时会导致数据丢失问题,
  • 使用redis就满足了数据共享,内存存储,key value结构的要求
  • 因为redis中没有cookid自动将浏览器对应到session域中,所以需要用手机号作为id
  • 在redis中存储验证码使用phone作为key,而存储用户信息则使用token,token返回给前端,然后将这个token数据存储到浏览器的数据域中,每次发送请求的时候都需要携带这个token
  • 前端每次发送请求的时候包含token的代码逻辑
    • image-20221121214015380

使用redis存储session数据需要进行的操作

  • 先在UserService中修改sendCode的代码 stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);保存验证码并设置验证码存活时间

  • 在校验验证码成功后保存用户信息的操作中可以通过UUID.randomUUID().toString(true);生成token来作为用户信息的key,true表示生成的token不包括下划线

  • 用户信息需要存储到hash类型中去,所以就需要将用户信息对象转换为map格式的,这个可以调用
    BeanUtil.beanToMap(user)将user转换为Map<string.Object>类型

    • 这里有一个注意点,因为我们使用的是StringRedisTemplate类型,所以存储到里边的数据的类型都需要是String类型,但是UserDto中的id是long类型,所以我们需要将这个id在存到map中将其转换为String类型

    • image-20221122150756731

    • Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));

  • 然后调用stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);将map类型的用户信息一次性存储到redis中去(put一次只能存一条,putAll一次才能存一个map类型)

  • 存储进去之后才可以通过expire设置这个hash类型的有效期stringRedisTemplate.expire(tokenKey,LOGIN_TOKEN_TTL,TimeUnit.MINUTES);

  • 最后需要将token返回给用户,让用户存储到浏览器中,并且发送的时候将token放到header中

  • 到拦截器中的StringRedisTemplate是不能自动注入的,因为这个类是自己写的,没有交给Spring容器构建,所以不会自动注入,所以需要自己将这个属性注入(构造方法注入),而构造方法中参数的获取就需要让创建这个拦截器的MvcConfig类(有@Configuration注解,是交给Spring容器管理的)来自动注入然后传递过来了

  • 然后就需要在拦截器中进行配置能够识别这是刚登陆的用户,具体操作就是通过
    request.getHeader("authorization");获取token(这个具体的名字应该是前端设定的)

  • 给token拼上前缀之后从redis中获取token对应的map

    String key = RedisConstants.LOGIN_TOKEN + token;
    Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
    
  • 然后调用BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);将对应的map转换为userDto对象,并将这个对象存储到ThreadLocal中,

    UserHolder.saveUser((UserDTO) userDTO);
    
  • 然后重新调用expire刷新token的存活时间

    stringRedisTemplate.expire(key,RedisConstants.LOGIN_TOKEN_TTL, TimeUnit.MINUTES)
    
  • 关于拦截器的一个注意点

    • 因为本身设置的拦截器是拦截用户访问那些需要用户信息的页面,而对其他页面放行了,但是这样的话如果在这个拦截器中刷新token,用户访问其他的页面token就不会刷新,所以需要新设置一个拦截器(这个拦截器面向的是所有的用户),这个拦截器不拦截请求,只是对有token的请求的token进行刷新并将用户信息存储到ThreadLocal中去,然后另一个拦截器来检查ThreadLocal中是否存有用户信息(这个拦截器面向的是被过滤后的用户)
    • 还有就是注册拦截器的时候可以通过.order(int num)设置拦截器的优先级,num越小表示拦截器的优先级越高font>(或者越先注册的拦截器优先级越高)
      • image-20221122153814471
      • image-20221122153830281
      • image-20221122153851335

小的优化点

  • 验证码保存到redis中是使用phone作为前缀的,虽然用户的手机号不会重复,但是可能有别的序列号和用户的手机号重复,所以最好给前边加一个login:code:这样的前缀
  • 固定的常量字符串和数字一般会自己在util中自定义一个类,然后将这些常量保存在这个类中,相当于自己的常量池(常量用static final修饰),我这里用的类是RedisConstants来保存redis有关操作对应的常量
  • 在设置redis中数据的存活时间的时候需要指定时间单位,这个可以在TimeUnit枚举类中获取对应的时间单位
  • 在redis中获取map后检查是否为空可以直接用.isEmpty()方法,因为如果数据为空,redis在返回的时候就会将这个封装成一个null的对象
  • hutool中BeanUtil包中的beanToMap和fillBeanWithMap可以实现bean转换为map和map转换为bean

设置缓存

  • image-20221122160925153

店铺缓存

  • 将操作放到service层,controller直接返回service层返回的Result结果接口,然后在ShopServiceImpl中先通过传过来的店铺id加上自己的自定义前缀获得key在redis中查询到对应的JSON字符串,

  • 然后判断JSON字符串是否为空,非空则将JSON字符串转换为Shop对象直接封装为Result返回

  • 为空或者Redis中没有数据就调用MP的getById函数通过id查询对应的店铺,然后将查询结果转成JSON存到redis中并将店铺信息返回

  • 对于店铺类型的缓存

    • 因为店铺类型数据是列表,所以需要通过fastjson中的JSON.parse()将数据转换为对象或使用JSON.toJSONString将对象转换为JSON通过StringRedisTemplate存储到redis的key value中
    • image-20221122193416711

小技巧

  • 如果是单个对象,可以用hutool中JSONUtil类下的toBean方法,第一个参数为JSON字符串,第二个参数为需要转换的对象.class

  • JSONUtil.toJsonStr(shop)可以将shop对象转换成JSON字符串

  • 如果是列表数据可以用alibaba的fashjson依赖中的JSON.parseJSON.toJSONString(list)将列表数据转换成JSON格式和列表JSON转换成List对象

    • image-20221122181300824

    • 列表数据如果还用JSONUtil.toBean()转换对象会报错的,因为这个转换针对的JSON是以{}开始结尾的

    •     <dependency>
              <groupId>com.alibaba</groupId>
              <artifactId>fastjson</artifactId>
              <version>1.2.76</version>
          </dependency>
      
  • 还有注意区分StringRedisTemplateRedisTemplate的区别,前者本身就重写了序列化类,后者的序列化需要自己重写

    • 当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候,那么你就使用StringRedisTemplate即可。

      但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是更好的选择。

      redisTemplate 中存取数据都是字节数组。当redis中存入的数据是可读形式而非字节数组时,使用redisTemplate取值的时候会无法获取导出数据,获得的值为null。可以使用 StringRedisTemplate 试试

缓存更新策略

  • image-20221122193517367

主动更新策略

  • 1.由缓存调用者在更新数据库的同时更新缓存(可控性高一些,使用的多)
    • 是删除缓存还是更新操作
      • 更新缓存:每次更新都会修改缓存,这样如果写大于读,就会多次的更新缓存是无效的
      • 删除缓存:当数据修改的时候直接将对应的缓存删除掉,然后当用户再次查询的时候重新建立缓存,这样不会造成无效操作,使用更广泛
    • 如何保证缓存与数据库的操作的同时或失败
      • 单体系统,将缓存和数据库操作放在同一个事务中
      • 分布式系统,利用TCC等分布式事务方案
    • 先操作缓存还是先操作数据库?
      • 先删缓存,在操作数据库
        • 这样会导致删完缓存,然后另一个线程过来查询,发现没有缓存,就从数据库中读取没有修改的数据重新写入缓存,其他用户再读的时候是错误的数据,这时候第一个线程再修改数据库中的值已经晚了
        • 因为删除缓存,查缓存,写缓存操作的速度比更新数据库快了很多,所以出现这种错误的情况还是比较多的
        • image-20221122195453873
      • 先操作数据库,再删缓存
        • 可能会出现的问题:一个线程来查数据的时候发现没缓存(可能是缓存过期了,可以能缓存压根就没进去过),然后取读取数据库的旧数据,读出来之后,修改数据库的进程来修改缓存了,先修改数据库库中的数据,然后要删除缓存(此时还没有缓存呢),然后刚刚从数据库中读取旧数据的进程将旧数据写入缓存,这也会造成缓存错误
        • 这种情况出现的概率不高,因为想这样首先得先没有缓存,然后得先从数据库中读出旧的数据,在读出写进这短暂的时间中修改进程要完成修改数据库,删除缓存的操作,这个难度还是比较大的
        • image-20221122201303786
        • 所以就用这种方案,真的出错了还会有超时删除缓存,从而更正缓存
    • 综上,最佳方案为
      • image-20221122201532438
  • 2.缓存和数据库整合为一个服务,由服务来维护一致性,调用者不用关心数据一致性问题
  • 3.调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致(如果出现宕机,缓存中的数据就丢失了,这样可能引发问题)
  • image-20221122194516958

实现商铺缓存和数据库内容读写一致

  • 知道先更改数据库,后更改缓存的流程后修改很简单,就是在ShopController的updateShop方法中调用shopService.update(shop);方法,然后在接口中新建方法,实现方法
  • 在实现方法中先判断传过来的shop是否有id,没有直接返回错误,有的话调用MP的updateById(shop)方法修改数据库,然后删除缓存中这个shop.id对应的缓存,直接返回ok即可

缓存穿透

  • 就是客户端请求的数据在缓存中和数据库中都不存在,这样的缓存永远不会生成,这样的话如果有人不停的访问空数据,就会一直出现缓存穿透的问题,从而导致数据库压力增大

解决方案

  • 1.缓存空对象
    • 优点:实现简单,维护方便
    • 缺点:额外的内存消耗
    • 可能造成短期的不一致(因为设置的缓存是有一个短暂的TTL的,这中间可能有不一致)
    • image-20221122211600042
  • 2.布隆过滤
    • 用户访问的时候是先访问布隆过滤器看这个数据是否存在,如果不存在直接拒绝,存在才会去访问redis,访问数据库
    • 布隆过滤器(实际上就是一个byte数组)是数据库中每一条数据通过某种hash算法计算出在byte数组上的位置,如果有数据就将这个位置置为1,这样就可以用极小的空间记录这条数据是否存在(布隆过滤器并不是100%的准确,如果说不存在,那就一定不存在,但是如果说存在,那也可能不存在)
    • 优点:内存占用较少,没有多余的key
    • 缺点:实现复杂,存在误判的可能
    • image-20221122213900035
  • 增强id的复杂度,避免id被猜的概率,并且让id有规律,可以通过基础格式校验筛选出无用的数据
  • 加强用户权限的控制
    • 访问权限做限制,对访问频率做限制
  • 做好热点参数的限流
    • 比如对访问次数多的进行一定的限流(可能是有人在恶意攻击)

使用方案一解决缓存穿透

  • image-20221122212257691
  • 首先在判断用户不存在后不要直接返回,先将这个请求的key及""的value存到redis中,并设置较短的存储时间,
  • 用户在从redis中获取数据后对数据进行校验,有真实内容的直接返回,为""的直接返回fail,剩下的再去数据库中进行查询

缓存雪崩

  • 缓存雪崩指同一时段大量的缓存key同时失效或者Redis服务宕机,当值大量请求到达数据库,带来巨大压力.

解决方案

  • 给不同的key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 缓存业务添加降级限流策略
    • 就是当发现redis出现故障时对服务进行降级,比如快速失败等等,不要让很多的请求全部压到数据库上去
  • 给业务添加多级缓存
    • nginx设置缓存,redis设置缓存,JVM内部设置本地缓存,最后到数据库

缓存击穿

  • 部分key过期造成的严重后果
  • 缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
  • image-20221122215112815

解决方案

  • 1.互斥锁
    • 同时只有一个线程能因为这个key不存在而访问数据库获得数据
    • image-20221122215638828
  • 2.逻辑过期
    • 就是不在redis中设置ttl,而是在value中设置一个时间,每次查询的时候看这个时间是否过期,如果过期就获取互斥锁,然后创建一个新的线程并将这个锁交给他,然后这个新线程进行查询数据库,重建缓存的操作,此时其他的线程如果访问的时候发现这个过期了,并且锁也没了,就直接吧旧的数据返回即可
    • image-20221122220129127
    • 两个一个维护了一致性,造成了可用性下降,一个维护了可用性,造成了一致性下降
      • image-20221122220504379

在项目中解决缓存穿透和缓存击穿问题

  • 解决缓存穿透问题

    • 主要就是对从数据库查为空的数据设置一个缓存,key是原来的key,值设置为"",并且设置一个较短的存活时间,这样在其他重复查询这个数据的时候就不会重新进入数据库,给数据库增加压力了,然后就是在从缓存中获取字符串的时候,如果结果为有数据的字符串表示有这个对象,直接返回结果即可,如果是""的字符串,表示是之前为了防止缓存穿透而设置的字符串(这个操作已经封装在queryWithPassThrough里了,所以queryWithPassThrough的返回不会有""这个可能,如果在方法体中查到"",那么也会返回null),如果从redis中查询结果为null,表示这个数据没有被查过,进入数据库中查询

    • // todo 这个通过id查询数据的方法已经解决了缓存穿透的问题,吧查到为空的数据也保存了
      public Shop queryWithPassThrough(Long id){
      String key = CACHE_SHOP_KEY + id;
      String shopJson = stringRedisTemplate.opsForValue().get(key);
      if (StrUtil.isNotBlank(shopJson)) {
      Shop shop = JSONUtil.toBean(shopJson, Shop.class);
      return shop;
      }
      // todo 这个是命中缓存中存放的""字符串了,是为了解决缓存穿透设置的,也代表没有店铺
      if (shopJson != null){
      return null;
      }
      // todo 从数据库中取这个店铺数据,如果店铺数据为null,直接返回
      Shop shop = getById(id);
      if (shop == null){
      stringRedisTemplate.opsForValue().set(key,""font>,CACHE_NULL_TTL,TimeUnit.MINUTES);
      return null;
      }
      // todo 这个是吧从数据库中查到的有数据的shop存到redis中 stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
      return shop;
      }

    • //    todo 这个通过id查询数据的方法已经解决了缓存穿透的问题,吧查到为空的数据也保存了
          public Shop queryWithPassThrough(Long id){
              String key = CACHE_SHOP_KEY + id;
              String shopJson = stringRedisTemplate.opsForValue().get(key);
              if (StrUtil.isNotBlank(shopJson)) {
                  Shop shop = JSONUtil.toBean(shopJson, Shop.class);
                  return shop;
              }
      //        todo 这个是命中缓存中存放的""字符串了,是为了解决缓存穿透设置的,也代表没有店铺
              if (shopJson != null){
                  return null;
              }
      //        todo 从数据库中取这个店铺数据,如果店铺数据为null,直接返回
              Shop shop = getById(id);
              if (shop == null){
                  stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                  return null;
              }
              stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
              return shop;
          }
      
  • 使用互斥锁的方式解决缓存击穿问题

    • 解决缓存击穿问题就是在解决缓存穿透问题基础上设置一个互斥锁,到数据库查找某一个对象的时候只有一个线程能进入数据库,其他都等待着

    • 通过setIfAbsent方法也就是 setnx key value 格式来将自己设置的用来定义作为锁的key以及自定义的一个值放到redis中,如果成功放进去了,就表示获得这个锁(这个锁要设置过期时间,防止获取锁后中间因为宕机等问题而无法释放锁)了,这个线程就可以进入数据库查询数据了,但是如果没有获得这个锁,就需要sleep一段时间,然后重新竞争这个锁

    • // 封装加锁操作

      private boolean tryLock(String key){
      Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
      return BooleanUtil.isTrue(flag);// 不能直接返回flag,因为如果中间出现问题导致flag为null后续会出错的
      }

      // 封装解锁操作

      private void unlock(String key){
      stringRedisTemplate.delete(key);
      }

      // 一次解决缓存穿透缓存击穿问题
      public Shop queryWithMutex(Long id){
      String key = CACHE_SHOP_KEY + id;
      String shopJson = stringRedisTemplate.opsForValue().get(key);
      if (StrUtil.isNotBlank(shopJson)){
      return JSONUtil.toBean(shopJson,Shop.class);
      }
      // todo 判断命中是否为空值(如果不为null就是""了),是为解决缓存穿透设置的缓存
      if (shopJson != null){
      return null;
      }
      // todo 实现缓存重建
      String lockKey = CACHE_LOCK_SHOP+id;
      Shop shop;
      try {
      boolean isLock = tryLock(lockKey);
      if (!isLock){
      // todo 这里每个线程都要进数据库中查一遍,感觉还是很费时,应该先到redis中查一遍,这样就能保证第一个缓存重建以后阻塞在哪里的线程不会再次进入数据库中查询重建数据
      Thread.sleep(10);
      queryWithMutex(id);//重新竞争锁
      }

      // 这里是为了防止之后重新竞争到锁的线程又操作数据库了,所以就让他们只访问redis缓存即可,因为第一个进入数据库的线程已经将数据更新

      ​ shopJson = stringRedisTemplate.opsForValue().get(key);
      ​ if (StrUtil.isNotBlank(shopJson)){
      ​ return JSONUtil.toBean(shopJson,Shop.class);
      ​ }
      // todo 获取锁成功,访问数据库
      ​ shop = getById(id);
      ​ Thread.sleep(100);//让操作数据库的线程休眠一下,模拟高并发的时候操纵数据库很费时的情况,这个是为了检验高并发的时候是不是走的redis缓存
      ​ if (shop == null){
      ​ // 如果查询到的数据为空,设置避免缓存穿透的缓存,并返回错误信息null
      ​ stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
      ​ return null;
      ​ }
      ​ stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
      ​ } catch (InterruptedException e) {
      ​ throw new RuntimeException(e);//这是为了解决sleep可能抛出的异常
      ​ }finally {
      // todo 释放互斥锁
      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);
      }
      public Shop queryWithMutex(Long id){
              String key = CACHE_SHOP_KEY + id;
              String shopJson = stringRedisTemplate.opsForValue().get(key);
              if (StrUtil.isNotBlank(shopJson)){
                  return JSONUtil.toBean(shopJson,Shop.class);
              }
      //        todo 判断命中是否为空值
              if (shopJson != null){
                  return null;
              }
      //        todo 实现缓存重建
              String lockKey = CACHE_LOCK_SHOP+id;
              Shop shop;
              try {
                  boolean isLock = tryLock(lockKey);
                  if (!isLock){
      //        todo 这里每个线程都要进数据库中查一遍,感觉还是很费时,应该先到redis中查一遍,这样就能保证第一个缓存重建以后阻塞在哪里的线程不会再次进入数据库中查询重建数据
                      Thread.sleep(10);
                      queryWithMutex(id);
                  }
                  shopJson = stringRedisTemplate.opsForValue().get(key);
                  if (StrUtil.isNotBlank(shopJson)){
                      return JSONUtil.toBean(shopJson,Shop.class);
                  }
      //        todo 获取锁成功,访问数据库
                  shop = getById(id);
                  Thread.sleep(100);//让操作数据库的线程休眠一下,模拟高并发的时候操纵数据库很费时的情况,这个是为了检验高并发的时候是不是走的redis缓存
                  if (shop == null){
          //            如果查询到的数据为空,返回错误信息
                      stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                      return null;
                  }
                  stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }finally {
      //            todo 释放互斥锁
                  unlock(lockKey);
              }
              return shop;
          }
      
  • 小技巧

    • 通过BooleanUtil.isTrue(flag);返回一个Boolean的对象可以防止这个对象为null的情况,保证返回的都是true或者false
    • stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);可以当互斥锁使用
    • 在catch中可以通过throw new RuntimeException(e);抛出异常
    • 在finally中将setIfAbsent设置的互斥锁解开(就是吧那个存储在redis中的字段删除)

封装缓存工具类

  • 设置一个缓存工具类,将设置缓存的方法代码抽象出来了

  • @Slf4j //打印日志的注解
    @Component //交给spring管理的注解
    public class CacheClient {
    private final StringRedisTemplate stringRedisTemplate;
    // todo 使用构造函数注入数据
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
    this.stringRedisTemplate = stringRedisTemplate;
    }
    // todo 设置逻辑过期缓存 并将传过来的value包装成了redisData对象,expireTime属性中存储的是逻辑过期时间
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
    RedisData redisData = new RedisData();
    redisData.setData(value);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }
    // todo 给id和返回值都定义了泛型,参数列表中Class<R>设置了泛型的推断
    // todo 通过Function<ID,R>通过使用函数式编程,可以实现将函数传递过去,ID参数类型的泛型,R返回值的泛型
    // todo 这个是解决缓存穿透问题的查询方法
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallBack,Long time,TimeUnit unit){
    String key = keyPrefix+id;
    String json = stringRedisTemplate.opsForValue().get(key);
    if (StrUtil.isNotBlank(json)){
    return JSONUtil.toBean(json,type);
    }
    if (json != null){
    return null;
    }
    R r = dbFallBack.apply(id);
    if (r == null){
    stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
    return null;
    }
    stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(r),time,unit);
    return r;
    }
    }

  • 调用缓存工具类的缓存穿透方法:this::getById可以替换为id2->getById(id2)
    Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

  • @Slf4j
    @Component
    public class CacheClient {
        private final StringRedisTemplate stringRedisTemplate;
    //    todo 使用构造函数注入数据
        public CacheClient(StringRedisTemplate stringRedisTemplate) {
            this.stringRedisTemplate = stringRedisTemplate;
        }
    //    todo 设置逻辑过期缓存
        public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
            RedisData redisData = new RedisData();
            redisData.setData(value);
            redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
        }
    //    todo 给id和返回值都定义了泛型,参数列表中设置了泛型的推断
    //    todo 通过Function<ID,R>通过使用函数式编程,可以实现将函数传递过去,ID是参数类型的泛型,R是返回值的泛型
    //    todo 这个是解决缓存穿透问题的查询方法
        public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallBack,Long time,TimeUnit unit){
            String key = keyPrefix+id;
            String json = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(json)){
                return JSONUtil.toBean(json,type);
            }
            if (json != null){
                return null;
            }
            R r = dbFallBack.apply(id);
            if (r == null){
                stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(r),time,unit);
            return r;
        }
    }