vlambda博客
学习文章列表

大厂Redis缓存雪崩,穿透,击穿,降级,预热等解决方案,面试官想知道的都在这!

在前面

一、缓存雪崩

1、什么是缓存雪崩?


缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。

2、缓存雪崩问题排查


  1. 在一个较短的时间内,缓存中较多的key集中过期

  2. 此周期内请求访问过期的数据,redis未命中,redis向数据库获取数据

  3. 数据库同时接收到大量的请求无法及时处理

  4. Redis大量请求被积压,开始出现超时现象

  5. 数据库流量激增,数据库崩溃

  6. 重启后仍然面对缓存中无数据可用

  7. Redis服务器资源被严重占用,Redis服务器崩溃

  8. Redis集群呈现崩塌,集群瓦解

  9. 应用服务器无法及时得到数据响应请求,来自客户端的请求数量越来越多,应用服务器崩溃

  10. 应用服务器,redis,数据库全部重启,效果不理想

3、有什么解决方案来防止缓存雪崩?


  1. 更多的页面静态化处理

  2. 构建多级缓存架构 Nginx缓存+redis缓存+ehcache缓存

  3. 检测Mysql严重耗时业务进行优化 对数据库的瓶颈排查:例如超时查询、耗时较高事务等

  4. 灾难预警机制

    • 监控redis服务器性能指标

    • CPU占用、CPU使用率

    • 内存容量

    • 查询平均响应时间

    • 线程数

  5. 限流、降级 短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问

  6. LRU与LFU切换 2. 数据有效期策略调整

    • 根据业务数据有效期进行分类错峰,A类90分钟,B类80分钟,C类70分钟

    • 过期时间使用固定时间+随机值的形式,稀释集中到期的key的数量

  7. 超热数据使用永久key

  8. 定期维护(自动+人工) 对即将过期数据做访问量分析,确认是否延时,配合访问量统计,做热点数据的延时 5. 加锁

4.总结


缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的出现(约40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。

二、缓存预热

1.什么是缓存预热


缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据。如图所示:


大厂Redis缓存雪崩,穿透,击穿,降级,预热等解决方案,面试官想知道的都在这!


如果不进行预热, 那么 Redis 初识状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。

2.问题排查


  1. 请求数量较高

  2. 主从之间数据吞吐量较大,数据同步操作频度较高


3.有什么解决方案?


前置准备工作:

  1. 日常例行统计数据访问记录,统计访问频度较高的热点数据

  2. 利用LRU数据删除策略,构建数据留存队列

     例如:storm与kafka配合

准备工作: 3. 将统计结果中的数据分类,根据级别,redis优先加载级别较高的热点数据 4. 利用分布式多服务器同时进行数据读取,提速数据加载过程 5. 热点数据主从同时预热

实施: 6. 使用脚本程序固定触发数据预热过程 7. 如果条件允许,使用了CDN(内容分发网络),效果会更好

4.总结

缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据

三、缓存穿透

1、什么是缓存穿透?


缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到对应key的value,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库

大厂Redis缓存雪崩,穿透,击穿,降级,预热等解决方案,面试官想知道的都在这!


2、有什么解决方案来防止缓存穿透?

1、缓存空值

如果一个查询返回的数据为空(不管是数据不存在,还是系统故障)我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过5分钟。通过这个设置的默认值存放到缓存,这样第二次到缓存中获取就有值了,而不会继续访问数据库

2、采用布隆过滤器BloomFilter

**优势:**占用内存空间很小,位存储;性能特别高,使用key的hash判断key存不存在

将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

在缓存之前在加一层BloomFilter,在查询的时候先去BloomFilter去查询key是否存在,如果不存在就直接返回,存在再去查询缓存,缓存中没有再去查询数据库

3.总结


缓存击穿访问了不存在的数据,跳过了合法数据的redis数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。应对策略应该在临时预案防范方面多做文章。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除。

四、缓存降级

降级的情况,就是缓存失效或者缓存服务挂掉的情况下,我们也不去访问数据库。我们直接访问内存部分数据缓存或者直接返回默认数据。

举例来说:

对于应用的首页,一般是访问量非常大的地方,首页里面往往包含了部分推荐商品的展示信息。这些推荐商品都会放到缓存中进行存储,同时我们为了避免缓存的异常情况,对热点商品数据也存储到了内存中。同时内存中还保留了一些默认的商品信息。如下图所示:


大厂Redis缓存雪崩,穿透,击穿,降级,预热等解决方案,面试官想知道的都在这!


降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。

五、缓存击穿

1、什么是缓存击穿?


在平常高并发的系统中,大量的请求同时查询一个key时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们称为缓存击穿


2、问题排查


  1. Redis中某个key过期,该key访问量巨大

  2. 多个数据请求从服务器直接压到Redis后,均未命中

  3. Redis在短时间内发起了大量对数据库中同一数据的访问

3、如何解决


1. 使用互斥锁(mutex key)

这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了。如果是单机,可以用synchronized或者lock来处理,如果是分布式环境可以用分布式锁就可以了(分布式锁,可以用memcache的add, redis的setnx, zookeeper的添加节点操作)。

大厂Redis缓存雪崩,穿透,击穿,降级,预热等解决方案,面试官想知道的都在这!


2. "提前"使用互斥锁(mutex key)

在value内部设置1个超时值(timeout1), timeout1比实际的redis timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中

3. "永远不过期"

  • 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

  • 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

    大厂Redis缓存雪崩,穿透,击穿,降级,预热等解决方案,面试官想知道的都在这!

4. 缓存屏障


class MyCache{

private ConcurrentHashMap<String, String> map;

private CountDownLatch countDownLatch;

private AtomicInteger atomicInteger;

public MyCache(ConcurrentHashMap<String, String> map, CountDownLatch countDownLatch,
AtomicInteger atomicInteger)
{
this.map = map;
this.countDownLatch = countDownLatch;
this.atomicInteger = atomicInteger;
}

public String get(String key){

String value = map.get(key);
if (value != null){
System.out.println(Thread.currentThread().getName()+"\t 线程获取value值 value="+value);
return value;
}
// 如果没获取到值
// 首先尝试获取token,然后去查询db,初始化化缓存;
// 如果没有获取到token,超时等待
if (atomicInteger.compareAndSet(0,1)){
System.out.println(Thread.currentThread().getName()+"\t 线程获取token");
return null;
}

// 其他线程超时等待
try {
System.out.println(Thread.currentThread().getName()+"\t 线程没有获取token,等待中。。。");
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 初始化缓存成功,等待线程被唤醒
// 等待线程等待超时,自动唤醒
System.out.println(Thread.currentThread().getName()+"\t 线程被唤醒,获取value ="+map.get("key"));
return map.get(key);
}

public void put(String key, String value){

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}

map.put(key, value);

// 更新状态
atomicInteger.compareAndSet(1, 2);

// 通知其他线程
countDownLatch.countDown();
System.out.println();
System.out.println(Thread.currentThread().getName()+"\t 线程初始化缓存成功!value ="+map.get("key"));
}

}

class MyThread implements Runnable{

private MyCache myCache;

public MyThread(MyCache myCache) {
this.myCache = myCache;
}

@Override
public void run() {
String value = myCache.get("key");
if (value == null){
myCache.put("key","value");
}

}
}

public class CountDownLatchDemo {
public static void main(String[] args) {

MyCache myCache = new MyCache(new ConcurrentHashMap<>(), new CountDownLatch(1), new AtomicInteger(0));

MyThread myThread = new MyThread(myCache);

ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executorService.execute(myThread);
}
}
}


4.总结


缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中redis后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个key的过期监控难度较高,配合雪崩处理策略即可。

六、总结

这些都是实际项目中,可能碰到的一些问题,也是面试的时候经常会被问到的知识点,实际上还有很多很多各种各样的问题,文中的解决方案,也不可能满足所有的场景,相对来说只是对该问题的入门解决方法。一般正式的业务场景往往要复杂的多,应用场景不同,方法和解决方案也不同,由于上述方案,考虑的问题并不是很全面,因此并不适用于正式的项目开发,但是可以作为概念理解入门,具体解决方案要根据实际情况来确定!

往期精选


CHOICENESS





是兄弟,就来 “kan”