基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
存击穿问题
将逻辑进行封装
package com.hmdp.utils.bean;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.utils.RedisData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.hmdp.constants.RedisConstants.*;
/**
* @author 凯哥Java
* @description 基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
* <p>
* * 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
* * 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓
* <p>
* 存击穿问题
* <p>
* * 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
* * 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
* @company
* @since 2022/11/26 21:31
*/
@Component
@Slf4j
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 将任意Java对象存放到缓存中-带有过期时间的
*
* @param key key
* @param value v
* @param time 过期时间
* @param unit 过期时间单位
*/
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
/**
* 将任意Java对象存放到缓存中-带有逻辑过期时间的
*
* @param key key
* @param value value
* @param time 逻辑过期时间-xx后过期.比如30min后国庆。time = 30 unit=xx
* @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 对象ID
* @param type 对象类型
* @param dbFallback 如果缓存不存在,就去数据库查询的函数式
* @param time 过期时间
* @param unit 单位
* @param <R> 返回对象
* @param <ID> 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;
String json = stringRedisTemplate.opsForValue().get(key);
//从缓存中查询
if (StrUtil.isNotBlank(json)) {
return JSONUtil.toBean(json, type);
}
if (null != json) {
//返回错误信息
return null;
}
//不存在,去数据库查询
R r = dbFallback.apply(id);
//5:数据库不存在,提示错误
if (Objects.isNull(r)) {
//防止缓存穿透,设置空对象
stringRedisTemplate.opsForValue().set(key, "", SHOP_CACHE_KEY_EMPTY_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
//写入缓存中
this.set(key, r, time, unit);
//返回
return r;
}
public <R, ID> R queryWithLogicalExpire(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);
//2:判断是否存在-不存在直接返回null
if (StrUtil.isBlank(json)) {
return null;
}
//从缓存中查询到,后在判断是否过期
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())) {
//未过期,直接返回
return r;
}
//过期了,重建.在重建的时候,使用互斥锁的
String lockKey = SHOP_LOCK_CACHE_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock) {
//启动线程重新构建
CACHE_EXECUTOR.submit(() -> {
try {
R dbR = dbFallback.apply(id);
//重新写入到Redis中
this.setWithLogicalExpire(keyPrefix, id, time, unit);
} finally {
//释放锁
unLock(lockKey);
}
});
}
return r;
}
/**
* 释放互斥锁
*
* @param key
*/
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
/**
* 获取互斥锁
*
* @param key
* @return
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "v1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
}
RedisData对象:
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
使用场景: 1:缓存穿透方法的调用:
Shop shop = cacheClient.queryWithPassThrough(SHOP_CACHE_KEY,id,Shop.class,this::getById,SHOP_CACHE_KEY_EMPTY_TTL, TimeUnit.MINUTES);
2:缓存击穿的方法调用
从这个工具类中,我们学到了以下几个封装常用的:
1:带有返回值和入参的泛型
2:函数式编程可以作为参数传递