Redis:手写一个Redis工具类,解决缓存穿透、雪崩、击穿问题?

发布时间 2023-04-21 00:36:10作者: 在博客做笔记的路人甲

代码

package com.lurenjia.redisspring.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.*;
import java.util.function.Function;

/**
 * @author lurenjia
 * @date 2023/4/20-20:40
 * @description 自制Redis工具类,实现了缓存空对象、逻辑过期时间。
 */
@Component
public class RedisUtils {
    /**
     * 空值缓存存在时间
     */
    public static final Long CACHE_NULL_TTL=2L;

    /**
     * 空值缓存存在时间的单位
     */
    public static final TimeUnit CACHE_NULL_TTL_UNIT=TimeUnit.MINUTES;

    /**
     * 互斥锁自动释放时间
     */
    public static final Long LOCK_TTL = 10L;

    /**
     * 互斥锁自动释放时间的单位
     */
    public static final TimeUnit LOCK_TTL_UNIT = TimeUnit.SECONDS;

    /**
     * 互斥锁的key前缀
     */
    public static final String LOCK_KEY = "lock:";

    /**
     * 线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = new ThreadPoolExecutor(
            2,
            5,
            3,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(3),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.DiscardOldestPolicy());

    private final StringRedisTemplate stringRedisTemplate;

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

    /**
     * 写入数据到缓存中,使用了hutool提供了工具类JSONUtil,将对象转为json字符串
     * @param key 键名
     * @param 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);
    }

    /**
     * 写入数据到缓存中,使用逻辑过期来进行缓存有效判定
     * @param key 键名
     * @param 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));
    }


    /**
     * 从缓存中获取数据,使用缓存空对象避免缓存穿透。
     * @param keyPrefix key前缀
     * @param id key
     * @param type value数据类型
     * @param dbFallback 回调方法,数据库操作
     * @param time 缓存时间
     * @param unit 时间单位
     * @return 1、缓存中有数据,直接获取到
     *         2、缓存中有空对象,直接返回null
     *         3、缓存不存在,进行数据库查询。
     *            3.1、数据存在,写入缓存中,放回数据
     *            3.2、数据不存在,缓存空数据,返回null
     * @param <R> 放回值类型
     * @param <ID> 查询条件类型
     */
    public <R ,ID> R getWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){
        //拼接key
        String key = keyPrefix +id;

        //1 获取 缓存数据 从redis中
        String json = stringRedisTemplate.opsForValue().get(key);

        //2 判断 数据 不为空值、null
        if(StrUtil.isNotBlank(json)){
            //返回数据
            return JSONUtil.toBean(json,type);
        }

        //3 判断 数据是个空值
        if(json!=null){
            //返回null
            return null;
        }

        //4 缓存不存在 进行数据库查询
        R r = dbFallback.apply(id);

        //5 数据不存在 缓存空对象
        if(r==null){
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,CACHE_NULL_TTL_UNIT);
            return null;
        }

        //6 数据存在 写入缓存中
        this.set(key,r,time,unit);
        return r;
    }

    /**
     * 从缓存中获取数据,使用逻辑过期避免缓存击穿。
     * @param keyPrefix key前缀
     * @param id key
     * @param type value数据类型
     * @param dbFallback 回调方法,数据库操作
     * @param time 缓存时间
     * @param unit 时间单位
     * @return 1、缓存中有数据,直接获取到
     *         2、缓存中有空对象,直接返回null
     *         3、缓存不存在,进行数据库查询。
     *            3.1、数据存在,写入缓存中,放回数据
     *            3.2、数据不存在,缓存空数据,返回null
     * @param <R> 放回值类型
     * @param <ID> 查询条件类型
     */
    public <R,ID> R getWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback, Long time, TimeUnit unit){
        //拼接key
        String key = keyPrefix+id;

        //1 获取 缓存数据 从redis中
        String json = stringRedisTemplate.opsForValue().get(key);

        //2 判断 数据为空
        if(StrUtil.isBlank(json)){
            //null
            return null;
        }

        //3 获取带逻辑时间的缓存对象 反序列化操作
        RedisData redisData = JSONUtil.toBean(json,RedisData.class);

        //4 获取数据对象
        R r = JSONUtil.toBean((JSONObject) redisData.getData(),type);
        //5 获取逻辑过期时间
        LocalDateTime expireTime = redisData.getExpireTime();

        //6 判断 缓存未过期,直接返回数据
        if(expireTime.isAfter(LocalDateTime.now())){
            return r;
        }

        //6 缓存已经过期 尝试获取互斥锁key
        String lockKey = LOCK_KEY+id;
        boolean isLock = tryLock(lockKey);

        //7 互斥锁获取成功
        if(isLock){
            //8 开启新线程,进行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try{
                    //数据库查询操作
                    R r1 = dbFallback.apply(id);
                    //缓存重建
                    this.setWithLogicalExpire(key,r1,time,unit);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }

        //9 返回过期数据
        return r;
    }


    /**
     * 获取互斥锁:在redis中存入一组key-value,若存入成功,则获取锁成功,若存入失败,则获取锁失败。
     * @param key 作为锁的key,value为1
     * @return
     */
    private boolean tryLock(String key){
        //写入一个数据到缓存中,如果数据已经存在,则不写入。
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",LOCK_TTL, TimeUnit.SECONDS);
        //避免空指针
        if(flag!=null){
            //自动拆箱
            return flag;
        }
        return false;
    }

    /**
     * 释放互斥锁:删除作为锁的key-value
     */
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }
}

/**
 *带有逻辑过期时间的缓存对象
 */
@Data
class RedisData{
    /**
     * 逻辑过期时间
     */
    private LocalDateTime expireTime;
    /**
     * 缓存数据
     */
    private Object data;
}