L1296686146 冗谪 2023-07-06 16:09 发表于陕西
封装工具类
今天主要是对商户查询的代码进行了工具类的封装,但是需要Redis实战——商户查询(二) 对这篇文章说明下错误的地方
-
Redis实战——商户查询(二) 问题
-
逻辑过期解决缓存击穿中的流程图中,获取互斥锁失败后,不应该是返回过期数据,而是线程休眠一会儿
image-20230706155017177
-
代码问题
RedisData<Shop> redisData = JSONUtil.toBean(shopString, RedisData.class); Shop shop = redisData.getData(); 需要修改为 RedisData redisData = JSONUtil.toBean(shopString, RedisData.class); Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class); shop = redisData.getData(); 需要修改为 shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
-
-
基于StringRedisTemplate封装一个缓存工具类
-
将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
-
将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
-
根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
-
根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
-
根据指定的key查询缓存,并反序列化为指定类型,需要利用互斥锁解决缓存击穿问题
-
/**
* 缓存工具类
* @author liang
*/
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
* @param key redis中的键
* @param value redis中的值
* @param time 时间
* @param unit 时间单位
*/
private void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
/**
* 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
* @param key
* @param value
* @param time
* @param unit
* @param <T>
*/
private <T> void setWithLogicalExpire(String key, T value, Long time, TimeUnit unit){
//封装value 设置逻辑过期时间
RedisData<T> redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//将数据写入redis中
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
/**
* 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
* @param keyPrefix redis中存储的键的前缀
* @param id 查询的id
* @param clazz 查询的对象类型
* @param function
* @param time TTL过期时间
* @param unit 过期时间单位
* @return
* @param <T>
*/
public <T,ID> T queryWithPassThrough(String keyPrefix, ID id, Class<T> clazz, Function<ID, T> function,Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 在redis中查询商铺信息
String s = stringRedisTemplate.opsForValue().get(key);
// 如果从redis中获取到了数据 则直接返回
if (StrUtil.isNotBlank(s)){
return JSONUtil.toBean(s, clazz);
}
// 通过id 在数据库中查询
T apply = function.apply(id);
// 数据库中不存在该值
if (ObjectUtil.isNull(apply)){
//将空值封装到Redis中
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
//返回空
return null;
}
//数据库中查询成功
this.set(key,apply,time,unit);
//返回数据
return apply;
}
/**
* 根据key 获取RedisData
* @param key
* @return
*/
private RedisData getRedisData(String key) {
String s = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(s)){
return null;
}
RedisData redisData = JSONUtil.toBean(s, RedisData.class);
return redisData;
}
/**
* 是否逻辑过期
* @param redisData
* @return true: 没有过期
*/
private boolean LogicalOverdue(RedisData redisData){
if (ObjectUtil.isNull(redisData)){
throw new NullPointerException("RedisData对象不能为空");
}
LocalDateTime expireTime = redisData.getExpireTime();
return expireTime.isAfter(LocalDateTime.now());
}
/**
* 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
* @param keyPrefix redis中存储的键的前缀
* @param lockKeyPrefix redis中存储的锁的键的前缀
* @param id
* @param clazz 查询的对象类型
* @param function
* @param time 逻辑过期时间
* @param unit 时间单位
* @return
* @param <ID>
* @param <T>
*/
public <ID, T> T queryWithLogicalExpire(String keyPrefix, String lockKeyPrefix, ID id, Class<T> clazz,
Function<ID, T> function, Long time, TimeUnit unit){
String key = keyPrefix + id;
RedisData redisData = this.getRedisData(key);
if (ObjectUtil.isNull(redisData)){
return null;
}
T t = JSONUtil.toBean((JSONObject) redisData.getData(), clazz);
//判断是否逻辑过期
if (this.LogicalOverdue(redisData)) {
//没有过期 返回数据
return t;
}
String lockKey = lockKeyPrefix + id;
try {
// 过期
//获取互斥锁
boolean b = this.tryLock(lockKey);
//获取锁失败
if (!b){
//线程休眠
Thread.sleep(50);
return queryWithLogicalExpire(keyPrefix, lockKeyPrefix, id, clazz, function, time, unit);
}
//获取锁成功应该进行再次检查redis缓存是否存在,即做DoubleCheck,如果存在则无需重建缓存
redisData = this.getRedisData(key);
if (ObjectUtil.isNull(redisData)){
return null;
}
t = JSONUtil.toBean((JSONObject) redisData.getData(), clazz);
//判断是否逻辑过期
if (this.LogicalOverdue(redisData)) {
//没有过期 返回数据
return t;
}
//开启新线程
executor.execute( ()-> {
T apply = function.apply(id);
this.setWithLogicalExpire(key, apply, time, unit);
});
}catch (Exception e){
throw new RuntimeException();
}finally {
this.unlock(lockKey);
}
return t;
}
/**
* 根据key在redis中获取 对象
* @param key
* @param clazz
* @return
* @param <T>
*/
private <T > T getBeanByKey(String key, Class<T> clazz){
String s = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(s)){
T t = JSONUtil.toBean(s, clazz);
System.out.println(t);
return t;
}
return null;
}
/**
* 根据指定的key查询缓存,并反序列化为指定类型,需要利用互斥锁解决缓存击穿问题
* @param keyPrefix redis中存储的键的前缀
* @param lockKeyPrefix redis中存储的锁的键的前缀
* @param id
* @param clazz 查询的对象类型
* @param function
* @param time 逻辑过期时间
* @param unit 时间单位
* @return
* @param <ID>
* @param <T>
*/
public <T, ID> T queryWithMutex(String keyPrefix, String lockKeyPrefix, ID id, Class<T> clazz,
Function<ID, T> function, Long time, TimeUnit unit){
String key = keyPrefix + id;
T t = this.getBeanByKey(key, clazz);
if (ObjectUtil.isNotNull(t)){
return t;
}
String lockKey = lockKeyPrefix + id;
try{
boolean b = tryLock(lockKey);
if (!b){
Thread.sleep(30);
return queryWithMutex( keyPrefix, lockKeyPrefix, id, clazz,
function, time, unit);
}
//获取锁成功应该进行再次检查redis缓存是否存在,即做DoubleCheck,如果存在则无需重建缓存
t = this.getBeanByKey(key, clazz);
if (ObjectUtil.isNotNull(t)){
return t;
}
t = function.apply(id);
if (ObjectUtil.isNull(t)){
stringRedisTemplate.opsForValue().set(key,"", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
this.set(key, t, time, unit);
}catch (Exception e){
}finally {
unlock(lockKey);
}
return t;
}
private int corePoolSize = 10;//核心线程数
private int maxPoolSize = 20;//最大线程数
private long keepAliveTime = 60;//非核心线程的空闲时间
private TimeUnit unit = TimeUnit.SECONDS;//时间单位
private LinkedBlockingQueue<Runnable> workQuery = new LinkedBlockingQueue<>();//任务队列
//创建线程池
private ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, workQuery);
/**
* 获取锁
* @param key
* @return
*/
private boolean tryLock(String key){
return stringRedisTemplate.opsForValue().setIfAbsent(key,"1", 10L,TimeUnit.SECONDS);
}
/**
* 释放锁
* @param key
*/
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}