本文转载请联系Java极客技术公众号。阿芬的一个小学女生最近刚从一家小互联网公司跳槽,最近面试不少。不善言辞的姑娘,功底还是可以的。她以前是做UI的,但是时间长了,觉得没什么意思,就开始学后端,然后慢慢的从原来的公司变成了后端开发,这也就是我们所说的“程序猿”。我最近采访了阿芬,讲述了她的面试经历。阿芬印象最深的一句话就是,我给你画个图,你看看,面试官是这么说的,怎么样?Redis穿透和雪崩你了解吗?为什么这么说,因为面试官在谈到Redis的时候,面试官问的不再是“说说Redis的数据结构”。现在一问面试题,很多就开始问Redis的实际使用了。比如Redis有哪些架构模式?单机版、主从复制、哨兵机制、集群(代理型)、集群(直连型)你用过Redis分布式锁吗,是怎么实现的?你用过Redis作为异步队列吗?,你如何使用它?缺点是什么?什么是缓存穿透?如何避免?什么是缓存雪崩?对于这个问题,学妹配合了一波自己的UI技巧和口语讲解,所以顺利拿到了这个offer。也可能是因为学妹比较漂亮,身手也不差。所以,准备工作吧。来看看小学生画的是什么,入职前让面试官问了很多。缓存穿透如图:图是阿粉找小学妹特意画的。让我们来看看。看完图,相信大家也看到什么是缓存穿透了吧。也就是说,在我们的缓存系统中,也就是在Redis中,我们都是把我们的Key拿到Redis中去寻找Value。如果我们在Redis中找不到我们的数据,我们就会去数据库中寻找我们的数据。如果只是单次请求的话,那还算不上什么大问题,只能说是崩溃了,但是如果并发请求量很大的话,就会给我们的数据库造成很大的压力,这其实就是称为缓存渗透,渗透的严重后果是缓存雪崩。先说穿透,后面再说雪崩。那么什么会导致缓存被穿透呢?自编码问题一些恶意攻击和爬虫造成大量空命中。如果一个黑客对你公司的项目和数据库比较感兴趣,他可能会给你海量的不存在的ID,然后疯狂调用你的其中一个接口,这些不存在的ID当你去查询缓存的数据时,那里根本不是这样的东西。这时候就会有大量的访问数据库的请求。虽然数据可能会持续一段时间,但迟早人家会让你冷静下来。那么应该如何解决缓存穿透的问题呢?使用互斥量,当缓存失效时,先获取锁,再请求数据库。如果没有拿到锁,它会休眠一段时间,然后重试。采用异步更新策略,无论Key是否获取到值,都会直接返回。在Value值中维护缓存过期时间。如果缓存过期,则异步启动一个线程来读取数据库并更新缓存。需要进行缓存预热(在项目启动前加载缓存)操作。提供一种能够快速判断请求是否有效的拦截机制,例如使用Bloomfilter在内部维护一系列合法有效的key。快速判断请求中携带的Key是否合法有效。如果无效,直接返回。Bloomfilter其实是比较推荐的一种方式。Bloomfilter的实现原理是这样的:当一个变量加入到集合中时,通过K个映射函数将该变量映射到位图中的K个点,并将它们设置为1。查询某个变量时,我们只需要检查这些点是否都为1才能知道它是否在集合中的概率很高。如果这些点中有任何一个为0,则查询的变量一定不存在;如果都为1,那么被查询的变量很可能在。注意这里可能存在,但不一定存在!这就是布隆过滤器的基本思想。而当你说布隆过滤器的时候,也许这就是面试官想问你的问题。这时候你就要开始和面试官聊Bloomfilter了。下面继续用大家都想看的图来解释布隆过滤器。在四个映射函数之后,字符串“Java”在位图中有四个点设置为1。当我们需要判断“子游”这个字符串是否存在时,只需要对该字符串进行一次映射函数操作,得到4个1,就说明“Java”可能存在。注意语言,可能存在,不一定存在,那是因为映射函数本身就是一个hash函数,hash函数会产生碰撞,也就是说会有一个字符串可能是"Java1"之后samefour映射函数运行得到的四个点可能和“Java”一样,这种情况下我们说有误算。另外,这四个点上的1也有可能是四个不同的变量计算后得到的,并不能证明字符串“Java”一定存在。而我们使用Bloomfilter提供了一种拦截机制,可以快速判断请求是否有效,判断请求中携带的Key是否合法有效。如果无效,直接返回。阿芬的小学妹给面试官解释了这个操作之后,看来面试官对这个“程序员”有点印象了,然后顺带问了一句,什么是缓存雪崩?这个时候的雪崩是指当我们有多个请求访问缓存的时候,这个时候,缓存里面是没有数据的,也就是缓存同时大面积失效。这时又一波请求来了,结果请求都发到数据库了,导致数据库连接异常。它实际上与遍历类似但又有所不同。相同点是都在搞数据库。不同的是,缓存遍历是指并发检查同一条数据,而缓存雪崩是因为不同的数据都过期了,很多数据都找不到,所以有很多通过检查数据库来解决缓存雪崩的策略,而且都比较实用。例如:给缓存的过期时间加上一个随机值,避免集体失效。双缓冲。我们有两个缓存,缓存A和缓存B。缓存A的过期时间是20分钟,双缓存策略比较有意思,不给缓存B设置过期时间。当有请求来的时候,我们先从缓存A中获取,如果缓存A中有数据,则返回直接给他。如果没有数据,则直接从B获取数据,直接返回。同时我们启动一个更新线程来更新A缓存和B缓存。这就是双缓存策略。上面提到的处理缓存雪崩的情况其实是从代码上实现的,但是如果我们换个角度思考,也就是从架构的方向去思考,解决方案如下。限流、降级、熔断那么如何实现限流呢?说到限流降级,不能简单的解决Redis的问题,实际上是为了保证用户保护服务的稳定性。那么为什么要限流呢?如果简单的说是为了保证系统的稳定性,面试官估计要崩溃了。这和不说是不一样的。你必须举一个简单的例子才能严肃。现场面试官举例:假设,我们现在的程序可以处理10个请求,但是第二天,突然200多个请求凑到一起,整整20次,这时候,程序就凉了,但是如果第一天晚上领导跟你说你明天写的程序要处理200多个请求。这个时候是不是得想个办法,比如说能不能再写一个程序把requests分享出来?,这时候其实就相当于要求我们限流了。限流算法的漏桶算法也是一样的,下面我们来了解一下这个算法是如何在全图中实现的。如果一个水桶的眼睛是窄的,我们往里面装满水,我们可以看到水在以恒定的速度一滴一滴地往下滴。如果桶满了,水滴就不会继续滴进去,如果不满,就继续加水。其实是这样的水滴其实就相当于请求。如果桶没有满,它可以继续处理我们传入的请求。当桶满了就拒绝处理,任其溢出。前提是我们的水桶是一个固定的容器,水桶不能随着水的增加而变大,不然还用什么限流算法。一个简单的漏桶算法的实现:publicclassLeakyBucket{publiclongtimeStamp=System.currentTimeMillis();//当前时间publiclongcapacity;//桶的容量publiclongrate;//漏水速度publiclongwater;//当前水量(当前累计次数requests)publicbooleanggrant(){longnow=System.currentTimeMillis();//先执行漏水,计算剩余水量water=Math.max(0,water-(now-timeStamp)*rate);timeStamp=now;if((water+1)
