缓存穿透和击穿
1.缓存穿透问题的解决思路
缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
- 缓存空对象
- 优点:实现简单,维护方便
- 缺点:
- 额外的内存消耗
- 可能造成短期的不一致
- 布隆过滤
- 优点:内存占用较少,没有多余key
- 缺点:
- 实现复杂
- 存在误判可能
本章只介缓存空对象
缓存击穿问题及解决思路
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
- 互斥锁
- 逻辑过期
逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大
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 com.hmdp.entity.Shop;
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;
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
// 创建线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//添加互斥锁
public boolean trylock(String key) {
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 直接返回有可能会导致拆箱使值为空
return BooleanUtil.isTrue(aBoolean);
}
//释放锁
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
// 用于缓存穿透
public void set(String key, Object value, Long time, TimeUnit timeUnit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
}
//缓存重建 用于缓存击穿
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit) {
// 设置过期时间
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusMinutes(timeUnit.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), time, timeUnit);
}
// 缓存穿透
public <R, ID> R querywithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit) {
// 从redis获取数据
String key = keyPrefix + id.toString();
String Json = stringRedisTemplate.opsForValue().get(key);
// 判断是否存在
if (StrUtil.isBlank(Json)) {
// 存在返回
return JSONUtil.toBean(Json, type);
}
// 判断命中的是否为空值
if (Json != null) {
// 返回错误信息
return null;
}
// 不存在 查询数据库
R r = dbFallback.apply(id);
// 判断是数据库是否存在
if (r == null) {
// 不存在也存储空对象
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id.toString(), "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 存在则保存到redis中 返回数据
this.set(key, r, time, timeUnit);
return r;
}
// 逻辑过期
public <ID, R> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback,Long time, TimeUnit timeUnit) {
String key = keyPrefix + id;
// 从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;
}
// 过期需要进行缓存重建
// 获取互斥锁
boolean lock = trylock(key);
// 判断是否获取锁
if (lock) {
// 获取开启独立线程,重建线程池
CACHE_REBUILD_EXECUTOR.submit(() -> {
// 重建缓存
try {
// 查询数据库id
R r1 = dbFallback.apply(id);
// 缓存重建
this.setWithLogicalExpire(key,r1,time,timeUnit);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
unlock(key);
}
});
}
//返回过期商品信息
return r;
}
}