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.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
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, 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), time, unit);
}
/**
* 使用缓存空对象解决缓存穿透查询数据
*/
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);
}
// 到这里说明json是null或""或/t/n
// 判断命中的是否是空值,这里是做缓存穿透,如果是空值说明数据库中也没有该值
if (json != null) {
return null;
}
// 去数据库中查,让调用者自己写查询方法
R r = dbFallback.apply(id);
//数据库中不存在,返回空值
if (r == null) {
// 将空值写到redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回空值
return null;
}
// 存在写入redis
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;
// 1.从redis中查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(json)) {
return null;
}
// 命中,首先把json反序列化为对象
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 = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock) {
try {
// 重建缓存,先查数据库
R r1 = dbFallback.apply(id);
//再写入redis
this.setWithLogicalExpire(key, r1, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
}
// 这里返回的还是r,因为逻辑过期解决缓存击穿的逻辑是如果逻辑过期了,返回旧数据
return r;
}
/**
* 尝试获取锁
*
* @param key
* @return
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1");
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
*
* @param key
*/
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
}





