查询一个根本不存在的数据, 缓存和DB都不会命中, 白嫖了缓存层和DB 。 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。
缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
通常缓存穿透的原因如下:
这个也很好理解,需要注意的是对空对象,设置个过期时间,伪代码如下
String get(String key) { // 从缓存中获取数据 String cacheValue = cache.get(key) // 缓存为空 if (StringUtils.isBlank(cacheValue)) { // 从存储中获取 String storageValue = storage.get(key); cache.set(key, storageValue); // 如果存储数据为空, 需要设置一个过期时间(300秒) if (storageValue == null) { cache.expire(key, 60 * 5); } return storageValue; } else { // 缓存非空 return cacheValue; } }
一般也够用了,极限情况:恶意***,几千万个不存在的id , 都打到了DB,你也都对这几千万个id 作为key, 存到了redis , 虽然他们的value都是空,而且你也设置了过期时间。
但是你想一下,你这几千万次的DB查询,你也挺难过吧,并且你redis里缓存这几千万个key , 那宝贵的内存资源岂不是白白的浪费掉了。。。。。
所以你需要布隆过滤器。
看场景,取舍。
Redis进阶-布隆过滤器
由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉。
对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间 。 比如 5到10分钟之间的一个随机时间。
伪代码如下
String get(String key) { // 从缓存中获取数据 String cacheValue = cache.get(key); // 缓存为空 if (StringUtils.isBlank(cacheValue)) { // 从存储中获取 String storageValue = storage.get(key); cache.set(key, storageValue); //设置一个过期时间(300到600之间的一个随机数) int expireTime = new Random().nextInt(300) + 300; if (storageValue == null) { cache.expire(key, expireTime); } return storageValue; } else { // 缓存非空 return cacheValue; } }
缓存雪崩指的是缓存层支撑不住或宕掉后, 流量会像奔逃的野牛一样, 打向后端存储层.
由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会达到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。
预防和解决缓存雪崩问题, 可以从以下三个方面进行着手。
一般情况下,我们使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。
但是有两个问题如果同时出现, 可能就会对应用造成致命的危害:
在缓存失效的瞬间, 有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃 。
再通俗一点来说 :对于一些设置了过期时间的key,假设这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。 如果正好在高并发的时候,这个key过期了。。。。 大量的请求都打到了DB层,造成DB的负载非常大,甚至宕机。
要解决这个问题主要就是要避免大量线程同时重建缓存。
我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。
伪代码如下
String get(String key) { // 从Redis中获取数据 String value = redis.get(key); // 如果value为空, 则开始重构缓存 if (value == null) { // 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex String mutexKey = "mutext:key:" + key; if (redis.set(mutexKey, "1", "ex 180", "nx")) { // 从数据源获取数据 value = db.get(key); // 回写Redis, 并设置过期时间 redis.setex(key, timeout, value);13 // 删除key_mutex redis.delete(mutexKey); }// 其他线程休息50毫秒后重试 else { Thread.sleep(50); get(key); } } return value; }