缓存穿透案例
比如我们查找一个店铺信息,按照正常的流程来讲
- 首先,从redis查找看能否找到店铺信息,如果找不到从数据库中查找,否则返回
- 从数据库中查找是,如果数据库不存在,报错。如果存在返回结果,并把查询到的信息放入redis中
但是如果一个用户,来查找一个不存在的id,这时我们每次都会查找数据库,如果是恶意的,该用户一直查询不存在数据,这样数据库压力会很大。
解决方法:
- 首先,从redis查找看能否找到店铺信息,如果redis命中的话,判断是否为空值,如果为空直接结束程序,如果不为空返回店铺信息
- 如果redis不存在,这时请求数据库,如果数据库也不存在,这时将空值写入redis,防止再次请求时同样以不存在的id,来查找。如果数据库中存在,返回店铺信息,并加入redis
缓存穿透产生的原因是什么?
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
缓存穿透的解决方案有哪些?
- 缓存null值
- 布隆过滤
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
代码实现:
/**
* @Date: 2022/5/2 15:02
* 原来写的解决缓存 穿透问题 解决方法
*/
public Shop queryWithPassThrough(Long id){
//从redis查询缓存
String key= RedisConstants.CACHE_SHOP_KEY +id;
String shopJson = template.opsForValue().get(key);
// 存在返回 当查到放入的空值时 也就是"" 不会进入下面这个if
if(StrUtil.isNotBlank(shopJson)){
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否是空值 当查到放入的空值时 也就是"" 会进入下面这个if
if(shopJson!=null){
//说明查到了放入的空值
return null;
}
//不存在 根据id 查数据库
Shop byId = getById(id);
//数据库不存在报错
if(byId==null){
//将 空值写入redis
template.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
//存在 返回并放入redis
String s = JSONUtil.toJsonStr(byId);
template.opsForValue().set(key,s,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return byId;
}
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
给不同key加 不同的ttl 随机值 这种代码就不再演示了
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
1.互斥锁
2.逻辑过期
互斥锁代码实现
/**
* @Date: 2022/5/2 15:00
*模拟锁 来解决缓存击穿问题
*
*/
private boolean tryLock(String key){
//使用setnx
Boolean flag = template.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key){
template.delete(key);
}
public Shop queryWithMutex(Long id){
String key=RedisConstants.CACHE_SHOP_KEY+id;
String shopJson = template.opsForValue().get(key);
// 存在返回 //判断命中的是否是空值 当查到放入的空值时 也就是"" 不会进入下面这个if
// 这里是为了解决缓存穿透
if(StrUtil.isNotBlank(shopJson)){
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否是空值 当查到放入的空值时 也就是"" 会进入下面这个if
if(shopJson!=null){
//说明查到的是空值
return null;
}
//实现缓存重建
//获取互斥锁
String lockKey=RedisConstants.LOCK_SHOP_KEY+id;
Shop shop =null;
try {
boolean isLock = tryLock(lockKey);
//判断获取是否成功
if(!isLock){
//没有拿锁成功 等待一会 再次递归调用
Thread.sleep(50);
//递归调用的过程中 可能拿到了别人新存入redis的key
return queryWithMutex(id);
}
//成功 根据id查询数据库
shop = getById(id);
//模拟并发问题 同时高并发访问 只会查询一次数据库
Thread.sleep(200);
if(shop==null){
template.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
template.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//一定要释放锁
unlock(lockKey);
}
return shop;
}
逻辑过期代码实现
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
//定义线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire(Long id){
//从redis查询缓存
String key= RedisConstants.CACHE_SHOP_KEY +id;
String shopJson = template.opsForValue().get(key);
// 存在返回 未命中返回空
if(StrUtil.isBlank(shopJson)){
return null;
}
//命中 重点判断是否过期
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Object data = redisData.getData();
Shop shop = JSONUtil.toBean((JSONObject) data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
//未过期 直接返回店铺信息
return shop;
}
//过期 缓存重建
String lockKey=RedisConstants.LOCK_SHOP_KEY+id;
boolean isLock = tryLock(lockKey);
if(isLock){
//todo 成功 开启独立线程 实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//测试时 设置为20 正常时候应该设为30分钟
this.saveShop2Redis(id,20L);
} catch (Exception e) {
e.printStackTrace();
}finally {
unlock(lockKey);
}
});
}
return shop;
}
public void saveShop2Redis(Long id,Long expireSecoonds) throws InterruptedException {
Shop shop = getById(id);
//测试 延迟 模拟高并发场景
Thread.sleep(200);
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSecoonds));
template.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
通过id查询 缓存 一般不会存在未命中的情况 因为key是不会过期的 一般热点key会提前添加到缓存 所以不会存在是否会过期的问题,查不到的问题
一致性问题:一致性不太能保证