代码
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; }