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

核心原理

  1、使用空缓存解决缓存穿透问题。

  2、使用逻辑过期解决缓存击穿问题。

 

实现代码

package com.lurenjia.pets_adoption.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import com.lurenjia.pets_adoption.dto.RedisData;

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

/**
 * @author lurenjia
 * @date 2023/4/20-20:40
 * @description 自制Redis工具类,提供数据存入redis中和从redis中取出数据。
 */
@Component
public class RedisUtils {
    /**
     * 空值缓存存在时间
     */
    public static final Long CACHE_NULL_TTL=5L;

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

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

    /**
     * 互斥锁自动释放时间的单位
     */
    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 key
     * @return 为null、“”、“ ”时,返回null,否则返回json字符串
     */
    public String get(String key){
        String json = stringRedisTemplate.opsForValue().get(key);
        //2 判断 数据 不为空值、null
        if(StrUtil.isNotBlank(json)){
            //返回数据
            return json;
        }
        return null;
    }

    /**
     * 写入数据到缓存中,使用逻辑过期来进行缓存有效判定
     * @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、缓存中没有数据,返回null
     *         2、缓存中有数据对象(带逻辑过期时间),进一步获取到缓存对象和逻辑过期时间
     *         3、根据逻辑过期时间,判断缓存是否过期
     *            3.1、数据未过期,返回数据
     *            3.2、数据已过期,获取互斥锁
     *              3.2.1、互斥锁获取失败,返回过期数据
     *              3.2.2、互斥锁获取成功,创建一个线程进行缓存重建,返回过期数据。缓存重建完成后释放互斥锁。
     * @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);
    }
}

 

 Demo示例

  查询条件封装为一个对象,示例为:把宠物信息列表储存到缓存中,从缓存中获取列表信息。

一、查询条件封装

/**
 * @author lurenjia
 * @date 2023/4/21-12:30
 * @description 宠物页码查询条件
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PetsQueryList {
    private Integer page;
    private Integer pageSize;
    private Pets pets;
    private HttpSession session;

    /**
     * toString:1_5_1
     * @return 页码_页码大小_用户类型(1 管理员,0 非管理员)
     */
    @Override
    public String toString(){
        Integer userType = ((Users) session.getAttribute("user")).getUserType()==1?1:0;
        return page+"_"+pageSize+"_"+userType;
    }
}

二、从数据库获取数据

    /**
     * 从数据库获取指定页码的宠物数据
     * @param petsQueryList
     * @return
     */
    private Page getPetsByDB(PetsQueryList petsQueryList){
        //取出查询条件
        HttpSession session = petsQueryList.getSession();
        Integer page = petsQueryList.getPage();
        Integer pageSize = petsQueryList.getPageSize();
        Pets pets = petsQueryList.getPets();

        //获取用户类型,管理员则为1,非管理员则为0
        Integer userType = ((Users) session.getAttribute("user")).getUserType()==1?1:0;//key:pet:list:1_2_1,页码,页码大小,用户类型
        String key = PET_LIST_PREFIX+page+"_"+pageSize+"_"+userType;

        //构建 查询条件对象
        LambdaQueryWrapper<Pets> queryWrapper = new LambdaQueryWrapper<>();
        //查询条件:宠物昵称、宠物状态,可能为空
        queryWrapper.like(StrUtil.isBlank(pets.getPetName()), Pets::getPetName, pets.getPetName());
        queryWrapper.eq(pets.getPetStatus() != null, Pets::getPetStatus, pets.getPetStatus());
        queryWrapper.orderByDesc(Pets::getPetIndata);

        //如果不是管理员 已经过世的不显示
        if(userType!=1){
            queryWrapper.ne(Pets::getPetStatus,3);
        }
        //准备 页面数据对象
        Page<Pets> pageInfo = new Page<>(page, pageSize);
        //查询数据 从数据库中
        this.page(pageInfo, queryWrapper);

        //进行响应数据
        return pageInfo;
    }

三、从缓存中获取数据

    /**
     * 获取宠物仓库列表信息,从缓存中
     * @param page
     * @param pageSize
     * @param pets
     * @param session
     * @return
     */
    @Override
    public R<Page> getList(Integer page, Integer pageSize, Pets pets, HttpSession session) {
        //查询条件封装
        PetsQueryList petsQueryList = new PetsQueryList(page,pageSize,pets,session);

        //响应数据
        Page pageInfo;

        //第一次获取缓存 为null
        if(null==redisUtils.get(PET_LIST_PREFIX + petsQueryList)){
            pageInfo = this.getPetsByDB(petsQueryList);
            redisUtils.setWithLogicalExpire(PET_LIST_PREFIX+petsQueryList,pageInfo,1L,TimeUnit.MINUTES);
            return R.success(pageInfo);
        }

        //从缓存中拿数据
        pageInfo = redisUtils.getWithLogicalExpire(
                PET_LIST_PREFIX,
                petsQueryList,
                Page.class, this::getPetsByDB, 1L, TimeUnit.MINUTES);

        //进行响应数据
        return R.success(pageInfo);
    }

 

posted @ 2023-04-21 00:28  在博客做笔记的路人甲  阅读(95)  评论(0编辑  收藏  举报