简单的Redis工具类

  • 作者: 凯哥Java(公众号:凯哥Java)
  • Redis
  • 时间:2022-11-26 21:20
  • 3180人已阅读
简介 基于StringRedisTemplate封装一个缓存工具类,满足下列需求:方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题方法4

🔔🔔🔔好消息!好消息!🔔🔔🔔

有需要的朋友👉:联系凯哥 微信号 kaigejava2022

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

  • 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间

  • 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓

存击穿问题

  • 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题

  • 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

将逻辑进行封装

25ab93f19fde72b72f79fde321e51a1d.png

package com.hmdp.utils.bean;

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.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.utils.RedisData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

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

/**
 * @author 凯哥Java
 * @description 基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
 * <p>
 * * 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
 * * 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓
 * <p>
 * 存击穿问题
 * <p>
 * * 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
 * * 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
 * @company
 * @since 2022/11/26 21:31
 */
@Component
@Slf4j
public class CacheClient {


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

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }


    /**
     * 将任意Java对象存放到缓存中-带有过期时间的
     *
     * @param key   key
     * @param value v
     * @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对象存放到缓存中-带有逻辑过期时间的
     *
     * @param key   key
     * @param value value
     * @param time  逻辑过期时间-xx后过期.比如30min后国庆。time = 30 unit=xx
     * @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));
    }


    /**
     * 封装处理缓存穿透问题的
     *
     * @param keyPrefix  缓存的key的前缀
     * @param id         对象ID
     * @param type       对象类型
     * @param dbFallback 如果缓存不存在,就去数据库查询的函数式
     * @param time       过期时间
     * @param unit       单位
     * @param <R>        返回对象
     * @param <ID>       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;
        String json = stringRedisTemplate.opsForValue().get(key);
        //从缓存中查询
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }
        if (null != json) {
            //返回错误信息
            return null;
        }
        //不存在,去数据库查询
        R r = dbFallback.apply(id);
        //5:数据库不存在,提示错误
        if (Objects.isNull(r)) {
            //防止缓存穿透,设置空对象
            stringRedisTemplate.opsForValue().set(key, "", SHOP_CACHE_KEY_EMPTY_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        //写入缓存中
        this.set(key, r, time, unit);
        //返回
        return r;
    }


    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);
        //2:判断是否存在-不存在直接返回null
        if (StrUtil.isBlank(json)) {
            return null;
        }
        //从缓存中查询到,后在判断是否过期
        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 = SHOP_LOCK_CACHE_KEY + id;
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            //启动线程重新构建
            CACHE_EXECUTOR.submit(() -> {
                try {
                    R dbR = dbFallback.apply(id);
                    //重新写入到Redis中
                    this.setWithLogicalExpire(keyPrefix, id, time, unit);
                } finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }
        return r;
    }


    /**
     * 释放互斥锁
     *
     * @param key
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

    /**
     * 获取互斥锁
     *
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "v1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

}

RedisData对象:

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}


使用场景:
1:缓存穿透方法的调用:

95a76fe313624a4ea0e9d8acc2d47b8f.png

Shop shop = cacheClient.queryWithPassThrough(SHOP_CACHE_KEY,id,Shop.class,this::getById,SHOP_CACHE_KEY_EMPTY_TTL, TimeUnit.MINUTES);

2:缓存击穿的方法调用

59292ca2674803dfa5e3c0c815a2d2a7.png

从这个工具类中,我们学到了以下几个封装常用的:

29665c58f99dbdcdd5c3ff91919b9bec.png

1:带有返回值和入参的泛型

2:函数式编程可以作为参数传递

TopTop