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