功能02-商铺查询缓存033.功能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@Slf4jpublic 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         泛型     * @param        泛型     * @return 返回指定的类型对象     */    public  R queryWithPassThrough(            String keyPrefix, ID id, Class type, Function 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         泛型     * @param        泛型     * @return 返回指定的类型对象     */    public  R queryWithLogicalExpire(String keyPrefix, ID id, Class type,                                            Function 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 */@Servicepublic class ShopServiceImpl extends ServiceImpl        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缓存总结