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); }