一、工具类需求

对于 Redis 缓存,需要解决缓存穿透和缓存击穿问题,这样业务逻辑是重复的,对每个使用缓存的方法都要使用,因此我们需要封装缓存工具类,具体实现以下需求:

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

方法一 和 方法三解决普通的缓存问题
方法二 和 方法四解决热点 key 的问题

二、代码

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

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.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;

@Slf4j
@Component // 注入 —— 交给Spring 维护
public class CacheClient {


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

    // 构造方法
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }


    // 创建方法
    public void set(String key, Object value, Long time, TimeUnit timeUnit){ // 传一个 时间值 ,加一个时间单位
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }

    // 创建时设置逻辑过期
    public void setWithLogicExpire(String key, Object value, Long time, TimeUnit timeUnit){
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    // 查找 —— 解决缓存穿透问题 ——
    // 参数分别为 : 缓存 key 的前缀 , 缓存 key 的 id, 缓存对象的类型, 数据库查找的方法, 过期时间, 时间类型
    public <R, ID> R queryWithPassTrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit) {
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);

        // 开启线程池
        final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

        // 缓存存在,直接返回
        if(StrUtil.isNotBlank(json)){
            return JSONUtil.toBean(json, type);
        }

        // 缓存空值 —— 解决缓存穿透
        if(json != null){
            return null;
        }

        // 查数据库
        R r = dbFallback.apply(id);

        if(r == null){ // 数据库中为 null,缓存空值
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        } else { // 数据库不为空,缓存 json
            this.set(key, r, time, timeUnit);
        }

        return r; // 返回查询到的对象
    }


    // 利用互斥锁解决缓存击穿问题
    // 获取锁
    private boolean tryLock(String key){
        boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag); // 防止出现 null
    }

    // 释放锁
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

    // 使用逻辑过期的方式解决缓存击穿
    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit)  {

        String key = keyPrefix  + id;

        // 1. 从 Redis 中查询商户缓存
        String json = stringRedisTemplate.opsForValue().get(key);        // 2. 判断是否存在

        // 2. 未命中
        if(StringUtil.isNullOrEmpty(json)){
            return null;
        }
        // 3. 命中
        // 3.1 JSON 反序列化为 RedisData 对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject data = (JSONObject)redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 3.2 判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            // 3.2.1 未过期,直接返回
            return r;
        }
        // 3.2.1 过期,需要进行缓存重建
        // 3.2.1.1 获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 3.2.1.2 判断是否成功获取到互斥锁
        // 3.2.1.2.1 获取互斥锁失败,直接返回数据
        if(!isLock){
            return r;
        }
        // 3.2.1.2.2 获取互斥锁成功,在开启一个线程进行缓存重建,然后返回数据
        CACHE_REBUILD_EXECUTOR.submit(()->{
            try {
                // 查数据库
                R r1 = dbFallback.apply(id);
                // 写入缓存
                this.setWithLogicExpire(key, r1, time, timeUnit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                // 释放锁
                unlock(lockKey);
            }
        });
        return r;
    }
}

调用逻辑

public  Shop queryWithLogicalExpire(Long id)  {
        Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.SECONDS);
        return shop;
    }
public Shop queryByIdWithPassThrough(Long id){
        Shop  shop = cacheClient.queryWithPassTrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }

`