一、工具类需求
对于 Redis 缓存,需要解决缓存穿透和缓存击穿问题,这样业务逻辑是重复的,对每个使用缓存的方法都要使用,因此我们需要封装缓存工具类,具体实现以下需求:
- 将任何 Java 对象序列化为 json 并存储在 string 类型的 key 中,并且可以设置 TTL 过期时间
- 将任意 Java 对象序列化为 json 并存储在 string 类型的 key 中,并可为其设置逻辑过期时间 expireTime,用于解决缓存击穿问题
- 根据指定的 key 查询缓存,并且反序列化为指定类型,并且利用缓存空值的方式解决缓存穿透问题
- 根据指定的 key 查询缓存,并且反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
方法一 和 方法三解决普通的缓存问题
方法二 和 方法四解决热点 key 的问题
二、代码
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import io.netty.util.internal.StringUtil;
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_NULL_TTL;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
@Slf4j
@Component // 注入 —— 交给Spring 维护
public class CacheClient {
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
private final StringRedisTemplate stringRedisTemplate;
// 构造方法
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
// 创建方法
public void set(String key, Object value, Long time, TimeUnit timeUnit){ // 传一个 时间值 ,加一个时间单位
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
}
// 创建时设置逻辑过期
public void setWithLogicExpire(String key, Object value, Long time, TimeUnit timeUnit){
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
// 查找 —— 解决缓存穿透问题 ——
// 参数分别为 : 缓存 key 的前缀 , 缓存 key 的 id, 缓存对象的类型, 数据库查找的方法, 过期时间, 时间类型
public <R, ID> R queryWithPassTrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit) {
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
// 开启线程池
final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 缓存存在,直接返回
if(StrUtil.isNotBlank(json)){
return JSONUtil.toBean(json, type);
}
// 缓存空值 —— 解决缓存穿透
if(json != null){
return null;
}
// 查数据库
R r = dbFallback.apply(id);
if(r == null){ // 数据库中为 null,缓存空值
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
} else { // 数据库不为空,缓存 json
this.set(key, r, time, timeUnit);
}
return r; // 返回查询到的对象
}
// 利用互斥锁解决缓存击穿问题
// 获取锁
private boolean tryLock(String key){
boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag); // 防止出现 null
}
// 释放锁
private void unlock(String key){
stringRedisTemplate.delete(key);
}
// 使用逻辑过期的方式解决缓存击穿
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit) {
String key = keyPrefix + id;
// 1. 从 Redis 中查询商户缓存
String json = stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在
// 2. 未命中
if(StringUtil.isNullOrEmpty(json)){
return null;
}
// 3. 命中
// 3.1 JSON 反序列化为 RedisData 对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
JSONObject data = (JSONObject)redisData.getData();
R r = JSONUtil.toBean(data, type);
LocalDateTime expireTime = redisData.getExpireTime();
// 3.2 判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
// 3.2.1 未过期,直接返回
return r;
}
// 3.2.1 过期,需要进行缓存重建
// 3.2.1.1 获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 3.2.1.2 判断是否成功获取到互斥锁
// 3.2.1.2.1 获取互斥锁失败,直接返回数据
if(!isLock){
return r;
}
// 3.2.1.2.2 获取互斥锁成功,在开启一个线程进行缓存重建,然后返回数据
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
// 查数据库
R r1 = dbFallback.apply(id);
// 写入缓存
this.setWithLogicExpire(key, r1, time, timeUnit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unlock(lockKey);
}
});
return r;
}
}
调用逻辑
public Shop queryWithLogicalExpire(Long id) {
Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.SECONDS);
return shop;
}
public Shop queryByIdWithPassThrough(Long id){
Shop shop = cacheClient.queryWithPassTrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
return shop;
}
`