基于StringRedisTemplate
封装一个缓存工具类,满足以下需求:
- 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
- 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间,用于处理缓存击穿问题
- 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方法解决缓存穿透问题
- 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
方法1,方法3对应是普通的缓存,解决缓存穿透,设置TTL
方法2,方法4结合起来是解决热点key,解决击穿问题.
java
package com.hmdp.utils;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RedisData {
private LocalDateTime expireTime;//逻辑过期时间
private Object data;//万能存储数据的对象 对原来实体不需要修改 也可以使用继承
}
java
package com.hmdp.utils;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
@Slf4j
@Component //将来这个bean由Spring维护
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//方法1
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
//方法2
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)));
//写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
//方法三
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R>type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
String key = keyPrefix + id;
//查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
//判断是否存在
if(StrUtil.isNotBlank(json)){
//存在,直接返回
return JSONUtil.toBean(json,type);
}
//判断是否为空值
if(json!=null){
//返回一个错误信息
return null;
}
//不存在,根据id查询数据库
//传入函数有参有返回值的函数Function 参数类型ID 返回值类型R
//函数式编程 就是定义的时候参数传了个接口,用到时传入接口的实现类
R r = dbFallback.apply(id);
//不存在,返回错误
if(r==null){
//将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",time,unit);
//返回错误信息
return null;
}
this.set(key,r,time,unit);
return r;
}
//线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R>type,Function<ID,R>dbFallback,Long time, TimeUnit unit){
String key = keyPrefix + id;
//1.从redis查询商铺缓存 也可以用springcache
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isBlank(json)) {
//3.存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
R r = JSONUtil.toBean(data,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;
boolean isLock = tryLock(lockKey);
//6.2 判断是否获取锁成功
//注意: 获取锁成功应该再次检测redis缓存是否过期,做DoubleCheck.如果存在则无需重建缓存
if(isLock){
//6.3 成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() ->{
//查询数据库
R apply = dbFallback.apply(id);
//写入redis
this.setWithLogicalExpire(key,apply,time,unit);
});
}
//6.4 返回过期商铺信息
return r;
}
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
//需要转化为基本类型返回 如果直接return flag是有可能发生拆箱的,可能出现空指针
return BooleanUtil.isTrue(flag);
}
private void unlock(String key){
stringRedisTemplate.delete(key);
}
}
应用:
java
@Resource
private CacheClient cacheClient;
@Override
public Result queryById(Long id) {
//缓存穿透
//Shop shop = queryWIthPassThrough(id);
// id2 -> getById(id2) == this::getById
// Shop shop = cacheClient.
// queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);
//互斥锁解决缓存击穿
// Shop shop = queryWithMutex(id);
//逻辑过期解决缓存击穿
// Shop shop = queryWithLogicalExpire(id);
Shop shop =
cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY,id, Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);
if (shop==null) {
return Result.fail("店铺不存在!");
}
return Result.ok(shop);
}