水军大提督 发表于 2023-4-22 01:20:55

day04-商家查询缓存03

功能02-商铺查询缓存03

3.功能02-商铺查询缓存

3.6封装redis工具类

3.6.1需求说明

基于StringRedisTemplate封装一个工具列,满足下列需求:
方法1:将任意Java对象序列化为json,并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json,并存储在string类型的key中,并且可以设置逻辑过期时间,用户处理缓存击穿问题(针对热点key)
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题(针对热点key)
3.6.2代码实现

(1)创建redis工具类,封装上述方法
package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

/**
* @author 李
* @version 1.0
* 封装redis工具类
*/
@Component
@Slf4j
public class CacheClient {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

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

    /**
   * 将任意Java对象序列化为json,并存储在string类型的key中,
   * 并且可以设置逻辑过期时间,用户处理缓存击穿问题(针对热点key)
   *
   * @param key   缓存的key值
   * @param value 缓存的value值
   * @param time过期时间值
   * @param unit过期的时间单位
   */
    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));
    }

    /**
   * 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
   *
   * @param keyPrefix查询的key值的前缀
   * @param id         查询的key值的后缀
   * @param type       要转换的Class类型
   * @param dbFallback 传入的函数
   * @param time       过期时间值
   * @param unit       时间单位
   * @param <R>      泛型
   * @param <ID>       泛型
   * @return 返回指定的类型对象
   */
    public <R, ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
      String key = keyPrefix + id;
      //redis查询缓存
      String json = stringRedisTemplate.opsForValue().get(key);
      //判断json是否存在
      if (StrUtil.isNotBlank(json)) {
            //存在,转为java对象并返回
            return JSONUtil.toBean(json, type);
      }
      //判断是否为"",如果是,说明该key是为了解决缓存穿透设置的空值
      if ("".equals(json)) {
            //返回错误信息
            return null;
      }
      //不存在,根据id查询数据库——使用函数式编程
      R r = dbFallback.apply(id);
      if (r == null) {//说明数据库中没有该数据
            //缓存空值,应对缓存穿透
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            //返回错误信息
            return null;
      }
      //r存在,则将其写入redis
      this.set(key, r, time, unit);
      return r;
    }

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

    /**
   * 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题(针对热点key)
   * @param keyPrefix查询的key值的前缀
   * @param id         查询的key值的后缀
   * @param type       要转换的Class类型
   * @param dbFallback 传入的函数
   * @param time       过期时间值
   * @param unit       时间单位
   * @param <R>      泛型
   * @param <ID>       泛型
   * @return 返回指定的类型对象
   */
    public <R, ID> R queryWithLogicalExpire(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);
      //这里不再考虑缓存穿透问题,因为key永不过期
      if (StrUtil.isBlank(json)) {
            //如果未命中,说明不是热点key,直接返回null
            return null;
      }
      //如果命中
      //先把json反序列化为对象
      RedisData redisData = JSONUtil.toBean(json, RedisData.class);
      R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
      LocalDateTime expireTime = redisData.getExpireTime();
      //判断是否逻辑过期
      if (expireTime.isAfter(LocalDateTime.now())) {
            //未过期,直接返回信息
            return r;
      }
      //过期,获取互斥锁
      String lockKey = LOCK_SHOP_KEY + id;
      boolean isLock = tryLock(lockKey);
      if (isLock) {//成功获取互斥锁
            //开启独立线程
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                  //重建缓存
                  //先查询数据库
                  R apply = dbFallback.apply(id);
                  //再存入reids缓存
                  this.setWithLogicalExpire(key, apply, time, unit);
                } catch (Exception e) {
                  throw new RuntimeException(e);
                }
                //释放互斥锁
                unLock(lockKey);
            });
      }
      //如果未获取互斥锁,直接返回旧数据
      return r;
    }

    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)修改ShopServiceImpl,调用封装好的方法,简化代码
package com.hmdp.service.impl;

import ...

/**
* 服务实现类
*
* @author 李
* @version 1.0
*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop>
      implements IShopService {
    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Resource
    private CacheClient cacheClient;

    @Override
    public Result queryById(Long id) {
      //缓存穿透
      //Shop shop =
      //      cacheClient.queryWithPassThrough
      //                (CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

      //缓存击穿方案(逻辑过期)
      Shop shop = cacheClient.queryWithLogicalExpire
                (CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.MINUTES);

      if (shop == null) {
            return Result.fail("店铺不存在!");
      }
      return Result.ok(shop);
    }

    @Override
    @Transactional
    public Result update(Shop shop) {
      ...
    }
}3.7缓存总结


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: day04-商家查询缓存03