1. 封装Redis工具类
方法一和三主要解决缓存穿透的问题;
方法二和四主要解决缓存击穿的问题;
2. 方法一和三
缓存穿透的封装;
private final StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 设置TTL过期时间set
* @param key
* @param value
* @param time
* @param unit
*/
public void set (String key, Object value, Long time, TimeUnit unit){
// 需要把value序列化为string类型
String jsonStr = JSONUtil.toJsonStr(value);
stringRedisTemplate.opsForValue().set(key, jsonStr, time, unit);
}
/**
* 缓存穿透功能封装
* @param id
* @return
*/
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
//1. 从Redis中查询商铺缓存
String Json = stringRedisTemplate.opsForValue().get(key);
//2. 判断是否存在
if(StrUtil.isNotBlank(Json)){
//3. 存在,直接返回
return JSONUtil.toBean(Json, type);
}
// 这里要先判断命中的是否是null,因为是null的话也是被上面逻辑判断为不存在
// 这里要做缓存穿透处理,所以要对null多做一次判断,如果命中的是null则shopJson为""
if("".equals(Json)){
return null;
}
//4. 不存在,根据id查询数据库
R r = dbFallback.apply(id);
if(r == null) {
//5. 不存在,将null写入redis,以便下次继续查询缓存时,如果还是查询空值可以直接返回false信息
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6. 存在,写入Redis
this.set(key, r, time, unit);
//7. 返回
return r;
}
3. 方法二和四
解决缓存击穿
/**
* 设置逻辑过期set
* @param key
* @param value
* @param time
* @param unit
*/
public void setWithLogicExpire (String key, Object value, Long time, TimeUnit unit){
// 设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 需要把value序列化为string类型
String jsonStr = JSONUtil.toJsonStr(redisData);
stringRedisTemplate.opsForValue().set(key, jsonStr);
}
// 定义线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 缓存击穿(逻辑过期)功能封装
* @param id
* @return
*/
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback
String key = keyPrefix + id;
//1. 从Redis中查询商铺缓存
String Json = stringRedisTemplate.opsForValue().get(key);
//2. 判断是否存在
if(StrUtil.isBlank(Json)){
//3. 不存在,直接返回(这里做的事热点key预热,所以已经假定热点key已经在缓存中)
return null;
}
//4. 存在,需要判断过期时间,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
//5. 判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
//5.1 未过期,直接返回店铺信息
return r;
}
//5.2 已过期,需要缓存重建
//6. 缓存重建
//6.1 获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
//6.2 判断是否获取锁成功
boolean isLock = tryLock(lockKey);
if(isLock) {
// 二次验证是否过期,防止多线程下出现缓存重建多次
String Json2 = stringRedisTemplate.opsForValue().get(key);
// 这里假定key存在,所以不做存在校验
// 存在,需要判断过期时间,需要先把json反序列化为对象
RedisData redisData2 = JSONUtil.toBean(Json2, RedisData.class);
R r2 = JSONUtil.toBean((JSONObject) redisData2.getData(), type);
LocalDateTime expireTime2 = redisData2.getExpireTime();
if(expireTime2.isAfter(LocalDateTime.now())) {
// 未过期,直接返回店铺信息
return r2;
}
//6.3 成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 重建缓存,这里设置的值小一点,方便观察程序执行效果,实际开发应该设为30min
// 查询数据库
R apply = dbFallback.apply(id);
// 写入redis
this.setWithLogicExpire(key, apply, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unLock(lockKey);
}
});
}
//7. 返回,如果没有获得互斥锁,会直接返回旧数据
return r;
}
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 这里的布尔类型自动拆箱可能会出现空指针异常
return BooleanUtil.isTrue(flag);
}
private void unLock(String key){
stringRedisTemplate.delete(key);
}