基于StringRedisTemplate封装一个缓存工具,主要有一下几个方法
方法1:将任意Java对象序列化为json并存储在String的指定key中且设置TTL
方法2:将任意Java对象序列化为json并存储在String的指定key中,并可以设置逻辑过期时间,用户处理缓存击穿问题
方法3:根据指定的key进行查询缓存,并反序列化为指定类型,利用缓存空值的办法解决缓存穿透问题
方法4:根据指定的key进行查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
此处使用到的互斥锁加锁解锁方法,在前面的文章有提及【Redis】Java通过redis实现简单的互斥锁_1373i的博客-CSDN博客https://blog.csdn.net/qq_61903414/article/details/130509874?spm=1001.2014.3001.5501
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@Data
class RedisData {
private LocalDateTime expireTime;
private Object data;
public RedisData(LocalDateTime expireTime, Object data) {
this.expireTime = expireTime;
this.data = data;
}
}
/**
* 封装解决redis 缓存击穿、缓存雪崩、缓存穿透等问题的方法
*/
@Component
@Slf4j
public class RedisUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private ObjectMapper objectMapper;
/**
* 存入时设置过期时间
* @param key
* @param value
* @param time
* @param unit
*/
public void set(String key, Object value, Long time, TimeUnit unit) throws JsonProcessingException {
stringRedisTemplate.opsForValue().set(key,objectMapper.writeValueAsString(value),time,unit);
}
/**
* 存入时设置逻辑过期
* @param key
* @param value
* @param time
* @param unit
*/
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) throws JsonProcessingException {
RedisData data = new RedisData(LocalDateTime.now().plusSeconds(unit.toSeconds(time)),value);
stringRedisTemplate.opsForValue().set(key,objectMapper.writeValueAsString(data));
}
/**
* 查询缓存,缓存穿透时缓存null解决
* @param id 要查询的id
* @param keyPrefix redis key的前缀
* @param type 要返回的类型
* @param dbFallback 查询数据库的方法的结果
* @param
* @param
* @return
* @throws JsonProcessingException
*/
public R getWithPassThrough(I id, String keyPrefix, Class type, Function dbFallback,Long time,TimeUnit unit) throws JsonProcessingException {
// 生成redis中的key
String key = keyPrefix + id;
// 查询redis
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
// 存在缓存,则返回
return objectMapper.readValue(json,type);
}
// 不存在时,判断是否是空值
if (json != null) {
// 返回错误信息
return null;
}
// 查询数据库
R result = dbFallback.apply(id);
// 判空
if (result == null) {
// 发送缓存穿透,缓存null
stringRedisTemplate.opsForValue().set(key,"",time,unit);
return null;
}
// 存在则重建缓存
stringRedisTemplate.opsForValue().set(key,objectMapper.writeValueAsString(result),time,unit);
return result;
}
/**
* 查询缓存,逻辑过期后异步重建缓存
* @param id
* @param keyPrefix
* @param type
* @param dbFallback
* @param time
* @param unit
* @param
* @param
* @return
* @throws JsonProcessingException
*/
public R get(I id, String keyPrefix,Class type,Function dbFallback,Long time,TimeUnit unit) throws JsonProcessingException {
// 构建查询redis的key
String key = keyPrefix + id;
// 查询redis
String json = stringRedisTemplate.opsForValue().get(key);
// 反序列化转为RedisData对象
RedisData data = objectMapper.readValue(json,RedisData.class);
// 获取数据
R r = JSONUtil.toBean((JSONObject) data.getData(),type);
// 获取逻辑时间
LocalDateTime expireTime = data.getExpireTime();
// 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 没有过期
return r;
}
// 到达此处已过期,重建缓存:加互斥锁--》查询数据库--》重建--》释放互斥锁
// 1.获取互斥锁
String lockKey = id + "_lock";
boolean isLock = LockByRedis.tryLock(lockKey);
if (isLock) {
// 2.加锁成功,开启线程重建缓存
new Thread(){
@Override
public void run() {
try {
// 3.查询数据库
R db = dbFallback.apply(id);
// 4.重建
stringRedisTemplate.opsForValue().set(key,objectMapper.writeValueAsString(db),time,unit);
} catch (JsonProcessingException e) {
e.printStackTrace();
} finally {
// 5.释放互斥锁
LockByRedis.unlock(lockKey);
}
}
}.start();
}
// 返回逻辑过期的数据
return r;
}
}
难点:在重建缓存时,我们需要去查询数据库,而查询数据库不同的reids缓存重建所需要查询的数据库表可能不同,其方法也可能不同。
解决:通过Function<参数类型,返回值>来把该查询数据库的方法交给方法的调用者进行传入