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

你们好,我是清风!以前分享过大厂Redis高并发场景设计,面试问的都在这!互联网大厂Java工程师面试指南——Redis篇,今天给小伙伴说说大厂面试高频必问点(缓存穿透,雪崩等问题)!以为不错的小伙伴能够关注点赞一下,感谢支持!java

写在前面

2020年面试必备的Java后端进阶面试题总结了一份复习指南在Github上,内容详细,图文并茂,有须要学习的朋友能够Star一下! GitHub地址: github.com/Java-Ling/J…git

1、缓存雪崩

一、什么是缓存雪崩?

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

二、缓存雪崩问题排查

  1. 在一个较短的时间内,缓存中较多的key集中过时
  2. 此周期内请求访问过时的数据,redis未命中,redis向数据库获取数据
  3. 数据库同时接收到大量的请求没法及时处理
  4. Redis大量请求被积压,开始出现超时现象
  5. 数据库流量激增,数据库崩溃
  6. 重启后仍然面对缓存中无数据可用
  7. Redis服务器资源被严重占用,Redis服务器崩溃
  8. Redis集群呈现崩塌,集群瓦解
  9. 应用服务器没法及时获得数据响应请求,来自客户端的请求数量愈来愈多,应用服务器崩溃
  10. 应用服务器,redis,数据库所有重启,效果不理想

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

  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%),配合其余策略一块儿使用,并监控服务器的运行数据,根据运行记录作快速调整。面试

2、缓存预热

1.什么是缓存预热

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

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

2.问题排查

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

3.有什么解决方案?

前置准备工做:数据库

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

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

    例如:storm与kafka配合
    复制代码

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

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

4.总结

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

3、缓存穿透

一、什么是缓存穿透?

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

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

一、缓存空值

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

二、采用布隆过滤器BloomFilter

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

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

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

3.总结

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

4、缓存降级

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

举例来讲:

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

降级通常是有损的操做,因此尽可能减小降级对于业务的影响程度。

5、缓存击穿

一、什么是缓存击穿?

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

二、问题排查

  1. Redis中某个key过时,该key访问量巨大
  2. 多个数据请求从服务器直接压到Redis后,均未命中
  3. Redis在短期内发起了大量对数据库中同一数据的访问

三、如何解决

1. 使用互斥锁(mutex key)

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

在这里插入图片描述

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

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

3. "永远不过时"

  • 从redis上看,确实没有设置过时时间,这就保证了,不会出现热点key过时问题,也就是“物理”不过时。
  • 从功能上看,若是不过时,那不就成静态的了吗?因此咱们把过时时间存在key对应的value里,若是发现要过时了,经过一个后台的异步线程进行缓存的构建,也就是“逻辑”过时

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的过时监控难度较高,配合雪崩处理策略便可。

6、总结

这些都是实际项目中,可能碰到的一些问题,也是面试的时候常常会被问到的知识点,实际上还有不少不少各类各样的问题,文中的解决方案,也不可能知足全部的场景,相对来讲只是对该问题的入门解决方法。通常正式的业务场景每每要复杂的多,应用场景不一样,方法和解决方案也不一样,因为上述方案,考虑的问题并非很全面,所以并不适用于正式的项目开发,可是能够做为概念理解入门,具体解决方案要根据实际状况来肯定!

相关文章
相关标签/搜索