缓存穿透:
客户端请求的数据在缓存中和数据库中都不存在,这样的缓存永远不会生效,这些请求会直接打到数据库中,造成数据库压力过大
解决方法:1.缓存空对象
//TODO 此方法中解决了缓存穿透问题(使用了缓存空对象)
public <R, ID> R queryWithThrow(String keyprefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyprefix + id;
//1 从redis中查询商铺缓存
String shop = stringRedisTemplate.opsForValue().get(key);
//2 判断是否存在
if (StrUtil.isNotBlank(shop)) {
//3 存在 直接返回
return JSONUtil.toBean(shop, type);
}
//判断缓存命中的是否是空值 如果是直接返回错误
if (shop != null) {
return null;
}
//4 reids不存在 根据ID 查找数据库
R r = dbFallback.apply(id);
//5 数据库不存在 返回错误 将null写入redis
if (r == null) {
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6 数据库存在 写入reids
this.set(key, r, time, unit);
return r;
}
2.布隆过滤
缓存雪崩:
同一时期大量的缓存key同时失效或redis宕机,导致大量的请求到达数据库,造成数据库压力过大
解决办法:1.给不同key的TTL设置随机值
2.利用redis集群提高服务的可用性
3.给缓存业务添加降级限流策略
4.给业务添加多级缓存
缓存击穿:
缓存击穿问题也叫热点key问题,指的是一个被高并发访问且重建业务复杂的key突然失效(TTL过期),请求会达到数据库,造成压力过大
缓存击穿存在的环境:假设key失效,线程1访问,缓存未命中,就要查询数据库进行缓存重建,因为重建业务复杂,所以重建很慢,恰好在这个时候,线程2,线程3,线程4.....同时访问,因为缓存并未写入,所以缓存并未命中,所以这些线程也会查询数据库并执行重建,对数据库的访问就会造成压力过大。
解决办法:1.互斥锁
//此方法中解决了缓存击穿问题(使用了互斥锁)和缓存穿透问题
public Shop quereWithMutex(Long id) {
//1 从redis中查询商铺缓存
String s = stringRedisTemplate.opsForValue().get(LOCK_SHOP_KEY + id);
//2 判断是否存在
//3 存在 返回商铺信息
if (StrUtil.isNotBlank(s)) {
return JSONUtil.toBean(s, Shop.class);
}
//判断空值
if (s != null) {
return null;
}
//4 实现缓存重建
//4.1 获取互斥锁
String lockkey = LOCK_SHOP_KEY + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockkey);
//4.2判断是否获取成功
if (!isLock) {
//4.3失败 休眠并重试
Thread.sleep(50);
return quereWithMutex(id);
}
//4.4根据id查询数据库
shop = getById(id);
//5 redis中不存在 数据库中也不存在 解决缓存穿透问题 写入null空值
if (shop == null) {
stringRedisTemplate.opsForValue().set(LOCK_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6存在 写入redis 返回信息 reids存的是JSON字符串
stringRedisTemplate.opsForValue().set(LOCK_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//7释放互斥锁
unLock(lockkey);
}
return shop;
}
//尝试获取锁
private boolean tryLock(String key) {
Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(b);
}
//释放锁
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
public void saveShop2Redis(Long id, Long expireSeconds) {
//1 查询店铺数据
Shop shop = getById(id);
//2 封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
redisData.setData(shop);
//3 写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
2.逻辑过期
//TODO 此方法中解决了缓存击穿问题(使用了逻辑过期)
public <R, ID> R quereWithLogicalExpire(String keyprefix, Long id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyprefix + id;
//1 从redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(LOCK_SHOP_KEY + id);
//2 判断是否存在
//3 存在 返回商铺信息
if (StrUtil.isBlank(shopJson)) {
return null;
}
//4 命中,先把json反序列化对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
LocalDateTime expireTime = redisData.getExpireTime();
JSONObject data = (JSONObject) redisData.getData();
R r = JSONUtil.toBean(data, type);
//5 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//5.1 过期直接返回
return r;
}
//5.2 已过期 需要进行缓存重建
//6 缓存重建
//6.1 获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
//6.2 判断是否获取锁成功
if (isLock) {
//6.3 成功 开启独立线程 实现缓存重建
//判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//过期直接返回
return r;
}
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//缓存重建
R r1 = dbFallback.apply((ID) id);
//写入redis
this.setWithLogicalExpire(key, r1, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unLock(lockKey);
}
});
}
//6.4 返回过期的商铺信息
return r;
}
private static java.util.concurrent.Executors Executors;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//尝试获取锁
private boolean tryLock(String key) {
Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(b);
}
private void unLock(String key) {
stringRedisTemplate.delete(key);
}