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 javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@Component
@Slf4j
public class CacheClient {
//线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//注入StringRedisTemplate
@Resource
StringRedisTemplate stringRedisTemplate;
//设置Redis内容(Redis TTL过期)
public void set(String key, Object value, Long expireTime, TimeUnit timeUnit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expireTime, timeUnit);
}
//设置Redis内容(逻辑过期)
public void setWithLogicTime(String key, Object value, Long expireTime, TimeUnit timeUnit) {
RedisData redisData = new RedisData();
redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expireTime)));
redisData.setData(value);
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
//缓存查询(解决缓存穿透问题)
public <R, ID> R queryShopWithPassthrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallBack
, Long expireTime, TimeUnit timeUnit) {
String key = keyPrefix + id;
//获取Redis中数据
String json = stringRedisTemplate.opsForValue().get(key);
//是否有值
if (StrUtil.isNotBlank(json)) {
//有值则直接返回
R bean = JSONUtil.toBean(json, type);
return bean;
}
//是否为空字符串
if (json != null) {
return null;
}
//获取数据库中数据并转化为R类型
R r = dbFallBack.apply(id);
String rStr = JSONUtil.toJsonStr(r);
//数据库和缓存中都不存在,在Redis中存放空字符串
if (r == null) {
//存入空字符串,防止缓存穿透
this.set(key, "", expireTime, timeUnit);
return null;
}
//数据库中存在,缓存不存在,则存入Redis
this.set(key, rStr, expireTime, timeUnit);
return r;
}
//缓存查询(使用逻辑过期解决缓存击穿)
public <R, ID> R queryShopWithLogicTime(String Cachekey, String lockkey, ID id, Class<R> type, Function<ID, R> dbToRedis
, Long logicExpireTime, TimeUnit timeUnit) {
//Redis的Key
String key = Cachekey + id;
//从Redis中获取JSON格式数据
String redisDataEntries = stringRedisTemplate.opsForValue().get(key);
//是否有值
if (StrUtil.isBlank(redisDataEntries)) {
//为空则直接返回
return null;
}
//将RedisData的JSON转化成bean对象
RedisData redisData = JSONUtil.toBean(redisDataEntries, RedisData.class);
//获取redisData中的Data(JSON格式,需要转化成bean)
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
//获取redisData的逻辑过期时间
LocalDateTime expireTime = redisData.getExpireTime();
//有值则判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//1.未过期则直接返回
return r;
}
//2.已过期
//2.1.尝试获取锁
String lockKey = lockkey + id;
boolean isLock = tryLock(lockKey);
//2.1.1. 获取锁成功
if (isLock) {
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//获取数据库并转换成R类型对象
R r1 = dbToRedis.apply(id);
//存入Redis
this.setWithLogicTime(key, r1, logicExpireTime, timeUnit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
});
}
//2.1.2. 获取锁失败
return r;
}
//尝试获取锁
private boolean tryLock(String key) {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(lock);
}
//删除锁
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
}
缓存穿透:
缓存穿透是指查询不存在的数据,由于缓存没有命中(因为数据根本就不存在),请求每次都会穿过缓存去查询数据库。如果这种查询非常频繁,就会给数据库造成很大的压力。
解决方法:
1.缓存空值:当客户端请求一个Redis和Mysql中都不存在的数据时,在Redis中存入一个空值,其Key为请求的参数。
因为此类为工具类,所以返回值不能确定,因此要设定一个泛型对象。
首先获取Redis中的数据,如果存在数据(不为空),则直接将数据转化成R类型的对象并且返回。
如果获取的数据为一个空字符串,则证明发生了缓存穿透,返回null。
如果不是以上两种(Redis中不存在该Key),则先从数据库查询并且转化为R类型的对象,如果该对象为空,则证明数据库和缓存中都不存在,在在Redis中存放空字符串。
如果不为空,则证明数据库中存在,缓存不存在。在Redis中存入该对象的JSON格式并且返回即可。
public <R, ID> R queryShopWithPassthrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallBack
, Long expireTime, TimeUnit timeUnit) {
String key = keyPrefix + id;
//获取Redis中数据
String json = stringRedisTemplate.opsForValue().get(key);
//是否有值
if (StrUtil.isNotBlank(json)) {
//有值则直接返回
R bean = JSONUtil.toBean(json, type);
return bean;
}
//是否为空字符串
if (json != null) {
return null;
}
//获取数据库中数据并转化为R类型
R r = dbFallBack.apply(id);
String rStr = JSONUtil.toJsonStr(r);
//数据库和缓存中都不存在,在Redis中存放空字符串
if (r == null) {
//存入空字符串,防止缓存穿透
this.set(key, "", expireTime, timeUnit);
return null;
}
//数据库中存在,缓存不存在,则存入Redis
this.set(key, rStr, expireTime, timeUnit);
return r;
}
2.使用布隆过滤器
缓存击穿:
缓存击穿是指某一个或少数几个数据被高频访问,当这些数据在缓存中过期的那一刻,大量请求就会直接到达数据库,导致数据库瞬间压力过大。
解决方法:
1.加互斥锁(不讨论缓存穿透的情况):
为了防止大量请求直接到达数据库,使用Redis中的互斥锁。如果获取互斥锁成功,则将数据存入Redis并且解锁,反之则证明已经有线程获取到了锁,其他线程只需要重试此方法。需要注意的是,使用setnx语句存储数据时,需要设定一个超时TTL,防止因为某些原因无法正常执行unlock语句。 (以下方法非工具类中的方法)
//使用互斥锁解决缓存击穿
private Result queryShopWithMutex(Long id) {
String shopEntries = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
Shop shop;
//是否有值
if (StrUtil.isNotBlank(shopEntries)) {
shop = JSONUtil.toBean(shopEntries, Shop.class);
return Result.ok(shop);
}
//添加互斥锁
//1.获取锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + "" + id;
try {
boolean tryLock = tryLock(lockKey);
//2.判断是否获取成功
//2.1.失败则休眠或重试
if (!tryLock){
Thread.sleep(50);
queryShopWithMutex(id);
}
//2.2.成功则获取数据库数据
//数据库中存在,缓存不存在
shop = getById(id);
String shopStr = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, shopStr, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
finally {
unLock(lockKey);
}
return Result.ok(shop);
}
private boolean tryLock(String key){
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(lock);
}
private void unLock(String key){
stringRedisTemplate.delete(key);
}
2.加逻辑过期时间:
逻辑过期指在Redis存数据时,将过期时间一同存入。如果使用了逻辑过期,需要线将数据在Redis中预热。
需要注意的是,使用逻辑过期时,如果一个数据的逻辑时间过期了,所有线程在更新线程重新写入数据前获取到的都是旧数据,当更新线程执行完后获取到新数据。
首先根据请求获取Redis中的数据redisDataEntries
,因为数据已预热过,所以如果不存在的话直接返回空值。
存在的话分已经过期和未过期
未过期: get到逻辑过期时间,并且和LocalDateTime.now()
进行比较,如果未过期的话说明此时redisDataEntries
仍可继续使用,将redisDataEntries
中的data转化为R类型的对象后返回即可。
已过期:如果逻辑时间和LocalDateTime.now()
过期了,则需要尝试获取锁,如果获取锁成功,则需要从线程池中选取一个“更新线程”执行setWithLogicTime()
方法。
如果获取锁失败的线程直接返回旧数据即可。
//缓存查询(使用逻辑过期解决缓存击穿)
public <R, ID> R queryShopWithLogicTime(String Cachekey, String lockkey, ID id, Class<R> type, Function<ID, R> dbToRedis
, Long logicExpireTime, TimeUnit timeUnit) {
//Redis的Key
String key = Cachekey + id;
//从Redis中获取JSON格式数据
String redisDataEntries = stringRedisTemplate.opsForValue().get(key);
//是否有值
if (StrUtil.isBlank(redisDataEntries)) {
//为空则直接返回
return null;
}
//将RedisData的JSON转化成bean对象
RedisData redisData = JSONUtil.toBean(redisDataEntries, RedisData.class);
//获取redisData中的Data(JSON格式,需要转化成bean)
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
//获取redisData的逻辑过期时间
LocalDateTime expireTime = redisData.getExpireTime();
//有值则判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//1.未过期则直接返回
return r;
}
//2.已过期
//2.1.尝试获取锁
String lockKey = lockkey + id;
boolean isLock = tryLock(lockKey);
//2.1.1. 获取锁成功
if (isLock) {
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//获取数据库并转换成R类型对象
R r1 = dbToRedis.apply(id);
//存入Redis
this.setWithLogicTime(key, r1, logicExpireTime, timeUnit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
});
}
//2.1.2. 获取锁失败
return r;
}
//尝试获取锁
private boolean tryLock(String key) {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(lock);
}
//删除锁
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
RedisData类:
package com.hmdp.utils;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}