封装Redis工具类

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

  • 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
  • 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓
  • 存击穿问题
  • 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
  •  方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

将逻辑进行封装--包含了Redis穿透和击穿的解决方法。

用到的知识点:

  • 泛型
  • 函数式编程
  • lambda表达式
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 com.hmdp.entity.Shop;
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.*;

@Slf4j
@Component
public class CacheClient {

    @Resource
    private StringRedisTemplate template;

    // 自定义线程池,可以封装成一个新的类进行引用
    private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(10);

    public void set(String key, Object value, Long time, TimeUnit unit) {
        template.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }
    

    public void setWithLogicExpire(String key, Object value, Long time, TimeUnit unit) {
        // 由于要设置逻辑过期时间,因此将其再封装成一个对象。
        RedisData redisData = new RedisData();
        redisData.setData(value);
        // 在现在时间的基础上再增加指定单位的时间--转换成秒
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        template.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    // 缓存击穿实现
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback,Long time, TimeUnit unit) {
        String shopKey = keyPrefix + id;
        // 1. 从redis中查询商铺缓存
        String jsonShop = template.opsForValue().get(shopKey);
        // 2. 判断缓存是否命中
        if (StrUtil.isNotBlank(jsonShop)) {
            // 3. 命中则直接返回商铺信息
            return JSONUtil.toBean(jsonShop, type);
        }
        // 可能是null 或者"";
        if (jsonShop != null) {
            return null;
        }
        R r = dbFallback.apply(id);
        // 4. 未命中根据id查询数据库
        if (r == null) {
            // 5. 空值写入redis。
            this.set(shopKey,"",time,unit);
            // 判断商铺是否存在-不存在返回报错信息
            return null;
        }
        // 6. 存在则写入Redis中
        this.set(shopKey,r,time,unit);
        // 7. 返回店铺信息
        return r;
    }

    // 缓存穿透- 基于逻辑缓存
    public <R, ID> R queryWithExpireTime(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback,Long time, TimeUnit unit) {
        String shopKey = keyPrefix + id;
        // 1. 从redis中查询商铺缓存
        String jsonShop = template.opsForValue().get(shopKey);
        // 2. 判断缓存是否命中
        if (StrUtil.isBlank(jsonShop)) {
            // 3. 命中则直接返回商铺信息
            return null;
        }
        // 缓存穿透判断可能是null 或者"";
        // 4. 命中
        // 4.1 判断缓存是否过期
        RedisData redisData = JSONUtil.toBean(jsonShop, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 4.2 未过期
            return r;
        }
        // 4.3 过期尝试获取锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean flag = tryLock(lockKey);
        if (flag) {
            String doubleCheck = template.opsForValue().get(shopKey);
            if (StrUtil.isNotBlank(doubleCheck)) {
                if (expireTime.isAfter(LocalDateTime.now())) {
                    // 没有过期就直接返回
                    RedisData doubleData = JSONUtil.toBean(jsonShop, RedisData.class);
                    return JSONUtil.toBean((JSONObject) doubleData.getData(), type);
                }
            }
            // 5. 获取锁成功开启独立线程
            EXECUTOR_SERVICE.submit(() ->{
                try {
                    // 重建缓存
                    R r1 = dbFallback.apply(id);

                    // 写入redis
                    this.setWithLogicExpire(shopKey,r1,time,unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 6. 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 7. 返回店铺信息
        return r;
    }

    public boolean tryLock(String key) {
        Boolean flag = template.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    public void unlock(String key) {
        template.delete(key);
    }


}

在ShopServiceImpl 中

@Resource
    private CacheClient cacheClient;

    // 查询前需要查询redis。--缓存击穿,存储穿透问题需要考虑
    @Override
    public Result selectById(Long id) {
        // 缓存穿透!
//        Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,
//                    this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);
        // 互斥锁解决缓存击穿问题。
//        Shop shop = queryWithMutex(id);
        // 逻辑过期解决缓存击穿问题
        Shop shop = cacheClient.queryWithExpireTime(CACHE_SHOP_KEY,id,Shop.class,
                   this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        return Result.ok(shop);
    }