你们好,我是清风!以前分享过大厂Redis高并发场景设计,面试问的都在这!及互联网大厂Java工程师面试指南——Redis篇,今天给小伙伴说说大厂面试高频必问点(缓存穿透,雪崩等问题)!以为不错的小伙伴能够关注点赞一下,感谢支持!java
2020年面试必备的Java后端进阶面试题总结了一份复习指南在Github上,内容详细,图文并茂,有须要学习的朋友能够Star一下! GitHub地址: github.com/Java-Ling/J…git
缓存雪崩是指在咱们设置缓存时采用了相同的过时时间,致使缓存在某一时刻同时失效,请求所有转发到DB,DB瞬时压力太重雪崩。因为原有缓存失效,新缓存未到期间全部本来应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存形成巨大压力,严重的会形成数据库宕机。github
缓存雪崩就是瞬间过时数据量太大,致使对数据库服务器形成压力。如可以有效避免过时时间集中,能够有效解决雪崩现象的出现(约40%),配合其余策略一块儿使用,并监控服务器的运行数据,根据运行记录作快速调整。面试
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就能够避免在用户请求的时候,先查询数据库,而后再将数据缓存的问题。用户直接查询事先被预热的缓存数据。如图所示:redis
若是不进行预热, 那么 Redis 初识状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库形成流量的压力。sql
前置准备工做:数据库
平常例行统计数据访问记录,统计访问频度较高的热点数据后端
利用LRU数据删除策略,构建数据留存队列缓存
例如:storm与kafka配合
复制代码
准备工做: 3. 将统计结果中的数据分类,根据级别,redis优先加载级别较高的热点数据 4. 利用分布式多服务器同时进行数据读取,提速数据加载过程 5. 热点数据主从同时预热服务器
实施: 6. 使用脚本程序固定触发数据预热过程 7. 若是条件容许,使用了CDN(内容分发网络),效果会更好
缓存预热就是系统启动前,提早将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,而后再将数据缓存的问题!用户直接查询事先被预热的缓存数据
缓存穿透是指用户查询数据,在数据库没有,天然在缓存中也不会有。这样就致使用户查询的时候,在缓存中找不到对应key的value,每次都要去数据库再查询一遍,而后返回空(至关于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库
一、缓存空值
若是一个查询返回的数据为空(无论是数据不存在,仍是系统故障)咱们仍然把这个空结果进行缓存,但它的过时时间会很短,最长不超过5分钟。经过这个设置的默认值存放到缓存,这样第二次到缓存中获取就有值了,而不会继续访问数据库
二、采用布隆过滤器BloomFilter
**优点:**占用内存空间很小,位存储;性能特别高,使用key的hash判断key存不存在
将全部可能存在的数据哈希到一个足够大的bitmap中,一个必定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
在缓存以前在加一层BloomFilter,在查询的时候先去BloomFilter去查询key是否存在,若是不存在就直接返回,存在再去查询缓存,缓存中没有再去查询数据库
缓存击穿访问了不存在的数据,跳过了合法数据的redis数据缓存阶段,每次访问数据库,致使对数据库服务器形成压力。一般此类数据的出现量是一个较低的值,当出现此类状况以毒攻毒,并及时报警。应对策略应该在临时预案防范方面多作文章。不管是黑名单仍是白名单,都是对总体系统的压力,警报解除后尽快移除。
降级的状况,就是缓存失效或者缓存服务挂掉的状况下,咱们也不去访问数据库。咱们直接访问内存部分数据缓存或者直接返回默认数据。
举例来讲:
对于应用的首页,通常是访问量很是大的地方,首页里面每每包含了部分推荐商品的展现信息。这些推荐商品都会放到缓存中进行存储,同时咱们为了不缓存的异常状况,对热点商品数据也存储到了内存中。同时内存中还保留了一些默认的商品信息。以下图所示:
降级通常是有损的操做,因此尽可能减小降级对于业务的影响程度。
在日常高并发的系统中,大量的请求同时查询一个key时,此时这个key正好失效了,就会致使大量的请求都打到数据库上面去。这种现象咱们称为缓存击穿
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. "永远不过时"
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); } } } 复制代码
缓存击穿就是单个高热数据过时的瞬间,数据访问量较大,未命中redis后,发起了大量对同一数据的数据库访问,致使对数据库服务器形成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个key的过时监控难度较高,配合雪崩处理策略便可。
这些都是实际项目中,可能碰到的一些问题,也是面试的时候常常会被问到的知识点,实际上还有不少不少各类各样的问题,文中的解决方案,也不可能知足全部的场景,相对来讲只是对该问题的入门解决方法。通常正式的业务场景每每要复杂的多,应用场景不一样,方法和解决方案也不一样,因为上述方案,考虑的问题并非很全面,所以并不适用于正式的项目开发,可是能够做为概念理解入门,具体解决方案要根据实际状况来肯定!