《如何与面试官处朋友》系列-缓存击穿、穿透、雪崩场景原理大调解
扫描二维码
随时随地手机看文章
前面我们提到分布式多级缓存架构的全貌,但总感觉少了些什么东西。在这样大的场景下面,如果遇到缓存使用问题那可咋办?但自古英雄出少年,相信此刻你已踏马西去,正走在寻找答案上得夕阳西下。每每面谈Redis大家肯定不陌生,反正就是各种被社会得毒打。上来就缓存问题(击穿、穿透、雪崩)三板斧,直接就是开门红,险些让我们招架不住。在这里我又日思夜想:
- 它们之间是如何造成的?
- 项目业务开发中应该注意些什么才能防范它们?
前言
说时迟那时快,对面依然不存在。正在低头玩手机的我,顿时感觉脸旁一阵凉风袭来,不由自主得抬起头来。只见一位光头大叔坐我对面,椅子顿时咯吱咯吱作响,顺手还摸了一把那锃亮锃亮的头。这雷厉风行、英姿飒爽的身段,外加上处处逼人的寒冷气息,犹如那剑指锋芒,让人背后感到针针发凉。
这难道是把传说中的敏捷开发修炼到高层时,所散发出的内力吗?犹如走出那六亲不认的步伐,走路带风
缓存业务的高效
面试官: 看你简历上写了很多缓存的内容,那先谈谈你对缓存的理解?为啥用它?
吒吒辉: 面试官您好,说到缓存,那你算是找对人了,我能给你干到天亮,目前几乎所有网站都会用它,我也是略知一二得。
其一
缓存一般指nosql,即非关系型数据库,它存储的数据并不像数据库那样有很强的逻辑关系,类似干拿钱-->办事-->走人的感觉,以K--->V形式来操作。
因无逻辑关系,所以操作数据时就能避免很多费时的逻辑数据计算操作,从而快速的获取到数据。
但Redis的K-V其实隐含了两层意思,不清楚你知道否?
因为K-V本身具备Hash的特征,所以就分别为外Hash和内Hash。
- 外Hash,指Redis的操作形式,即这种K-V的结果。从表现形式来的
- 内Hash,为Redis的hash数据类型,从存储的结构来看。Redis的hash的结构底层采用类似JAVA的HashMap。
面试官: 我记得那个Hash不是还有一个HashTable吗?Redis咋不用它?
首先,它们底层都基于 Hashtable 来实现的,区别就在于Hashtable是线程安全的,而Hashmap则相反。
因为Redis本身就是单线程,故它的模式就 决定它为线程安全。所以采用 Hashtable 这种支持多线程的线程安全机制就有点浪费。好比如你一个人是想咋玩就咋玩。
Redis-Hash采用数组+链表的形式共同组成HASH。
Redis真的的单线程吗?后面发文分析
其二
缓存多以内存为存储介质,远不是MySQL走磁盘能比拟的。 我直接给你拿京东的金士顿来做个比较,你老人家就明白了。
磁盘性能:
内存性能:
内存性能:2666MHz * 64bit(单通道,双通道则128bit) / 8(位到字节单位转换) = 21.328GB/s。这只是理论,实际发挥还要看内存控制器,实际上2666单条跑出来的数据在18~20GB/s差不多了。
差距:21328Mb/500Mb(SSD)=40.656。那你机械硬盘就会有上百倍的差距。一般部署服务器的磁盘不可能都是固态。而是固+机,简称:古(固)巨基(机)。也是出于成本的考虑,
面试官: 那固态和机械为啥差距这么大呢?
机械磁盘的读写是需要根据磁头臂的移动+磁头读写数据才能完成,它是由外到内的写入每一个扇区。
而固态硬盘会有多个闪存。每个闪存可能有4、8、16个闪存颗粒构成。读写时,可以同时多个闪存采用电信号去到存储位置中来进行读写,相当于并行的方式,所以比你磁盘转动得要快得多。
好比如传递口令的工作。前者是先跑到站点,在写下通知。然后才能继续跑到下一个站点进行通知。而后者则是直接通过电话口头传递指令给多个站点
闪存是什么?
闪存是一种存储介质,和内存最大的区别是断电后数据仍然不会丢失(和硬盘相似),数据删除不是以单个的字节为单位而是以固定的区块为单位,区块大小一般为256KB到20MB。
吒吒辉: 所以你像MySQL这样的东西在高并发请求下,那就很有问题,我在给你老人家打个比方
MySQL-3个关联表的数据结果,我给它安排到缓存中,在用内存来提提速,这不就是省去了数据在数据库中组装、从磁盘中读取的时间吗?
所以,用它,将可以大范围抵御高频率的用户访问请求,避免做重复而又复杂的计算工作。直接将其拦截到数据库上游,保护后方的机器。让你请求在高也得给我规规矩矩得。
你像拿电商首页、秒杀系统、热点推荐等数据,那都要100%考虑缓存得,当然业务需要提速的话,都是可以用得到的
一眼望去你的简历,就 缓存击穿 我心房
面试官: 嗯,不错,那使用缓存常会遇到缓存的3大问题,你先给说下缓存击穿是什么?
吒吒辉: 其实,缓存使用问题主要就集中在数据为脏数据、使用时缓存失效、缓存实例不可用等问题上。
而我们一切的方案都折中于满足于主要问题。而这“缓存击穿”就是缓存失效的情景。
什么是缓存击穿呢?
缓存击穿就是当你请求访问缓存Key时,恰巧它失效了。而这时数据还没更新到缓存中,外加上高频访问请求。相当于把缓存都给打穿了。
从而让这些透穿的请求分分钟把数据库给打得怀疑人生,甚至不能自理。犹如下面这感觉,表面风平浪静,实则早以暗藏杀机
为什么有这么大的杀伤力?
因为你原来的请求走的是缓存,现在它没了。所以数据库就需要多承受几十、上百倍的数据访问压力。
假设当时 10000/s 个请求,缓存能扛住9000/s 个请求,缓存一失效。此时10000 /s个请求全部落到数据库,那自然是招架不住。
这时一般数据库的CPU会飙到100%,服务器会卡死。可能DBA会直接重启一波。那不用多说,会直接再次陷入僵局。
当然这种规模访问,一般都有监控系统,在得出数据库负载过大的情况时,会发送报警的消息通知。然后再针对性想办法。
面试官:那你觉得这种情况要怎么做?
吒吒辉: 解铃还须系铃人,既然是在使用过程中遇到突变情况,那么就需要在业务&缓存上来提前进行搭桥。
一、业务层
1.1 加锁
如果缓存失效没拿到数据,就先拿到互斥锁然后去数据库查询数据,然后在返回结果,并重新设置缓存。
这样高频的请求访问就会被限制为1个请求。数据库的压力自然也会变小,后面相同请求继续走缓存。
public static getData(string $key) { //从缓存读取数据 $result = $redis->get($key); //缓存中不存在数据 if ($result ==null ) { //获取swoole互斥锁 $lock = new Swoole\Lock(SWOOLE_MUTEX); if ($lock->lock()) { $result = getDataFormMysql($key); //获取到数据库 if ($result != null)) setDataFromRedis($key, $result)); return $lock->unlock(); }else { //获取锁失败,则重试 usleep(10000); $result = getData($key;) } } return $result; }
但这加锁方案是不是会阻塞请求,影响用户体验呢?
还是一个递归的调用。没办法,你没抢到锁的请求只能等待或重试。不然你难道要打人爆力抢锁呀!!!
有什么方式可以改进?
用队列。
其实上面的锁还可改为最大重试的次数,不然如果遇到网络波动而导致请求阻塞,这样不断的进行重试获取锁,就会把机器给拖垮。当然网站请求量少,基本上没啥问题。但万事还得小心。
1.2. 队列
请求没拿到缓存,那么把请求放入到一个去重的队列中,相同查询请求保留一个即可。这时就可以返回客户端“您稍安勿躁,请等一下”,然后异步执行队列去获取数据库中的数据并更新到缓存中。后面来的请求就可读缓存中的数据。
这种情况下 都是直接响应用户, 那这样不是就有很多用户无法拿到数据吗?
如果你觉得这样不好,可以来个折中的办法,第一次的请求入队列,让它去读取数据并更新缓存。第二次乃至后面的请求在入队列时进行判断。如果发现会重复,那么就等0.1s,然后在去缓存里面拿数据返回。
这样就可以,不会一直等待数据。但等待是需要的。如果0.1s还不行,就返回提示内容给客户端,不让其持续等待。还能够省去争取锁的消耗。bingo
1.3. 无效时间
让缓存无失效时间。但最好设置一个计划任务,不然缓存可能有脏数据的问题,但更新不频繁或数据不怎么发生变化的业务没事
二、缓存层
缓存层面就是自主实现更新数据到缓存中,当然这个肯定是需要结合后台任务。比如缓存时间为10S。我反手一个定时任务就安排在9S,把数据更新到缓存中。或者考虑发布订阅模式,监听到频道来进行数据更新。
面试官:前面你提到加锁,那如果是分布式多实例的场景你要咋办?
你老说得确实是个问题,对应一般高流量的网站都是会采用分布式多机器的架构模式。这时候也会有并发的问题产生。
因为简单的互斥锁,只能满足单机器上的击穿问题。如果在分布式的并发场景下,就变成同时有多台机器拿到锁而访问到后端。
这样就达到了并行的访问形式,数据库也会因为请求量过大而造成并发访问。所以说这种情况下还是有很大压力。那要怎么解决呢?
咱们直接给他换成分布式锁就可以达到效果,比如用:Redis、memcache、zookeeper等来做。
面试官:说了这么多,你更倾向于那个呢?
吒吒辉:
我觉得还是要看业务场景,简单的肯定是锁和设置不过期的数据,对于后者可以通过订阅频道或定时任务来更新数据,防止有脏数据。
如果要用户体验好点的话,就搞队列吧,这样不用一直阻塞等待,在分布式的场景那分布式锁还是得来独自安排。
二眼回眸你的简历,那 缓存穿透 我心脏
面试官: 那你在谈谈缓存穿透吧?
缓存穿透就是当你请求访问缓存Key时,缓存本身的内容就没有它,数据库更无它的身影。这样的请求就会穿透缓存直达数据库。
如果这是一个高频请求,数据库就可能被恶意的打垮。就比如一种大网,请求就通过中间的缝隙穿透过去。好像漏网之鱼
为什么说被恶意的打垮呢?
原因就在,数据库本身就无这个key对应的数据,而缓存又依照数据库生存,那它就更没得了。
比如 拿商品id=-1的条件来查询;你说这不是恶意是啥?明明就没有,结果还是要来查询。明摆着给你穿小鞋。欲加之罪,何患无辞。
面试官: 那你面对这种情况会怎么来解决?咳咳,这个还需要大致分析下才有出路。
一、业务上:
1.1.访问限制黑名单
如果不想业务上加入太多不可控因子。那么可以在服务端加入访问限定黑名单机制。咋个意思?
用户请求发送到服务端时,服务端调用Redis-client先拿 EXISTS 判断缓存数据是否存在,如果发现没得值,就把当前key记录到访问限制黑名单列表,访问次数+1,并设置合适的过期时间。然后再去数据库读取数据。
如果访问的key是我们缓存中的内容,那么自然可以去数据库拿到数据,如果同一个key或几个key都一二再再而三拿不到数据,那么就肯定有问题。对应黑名单限制列表的访问次数自然也就上去了。
这样后面请求获取key时,就先去访问控制列表看看有没得它(key)这个刺儿头,有,我直接头都不回的给你拒绝掉,还顺手给你一大嘴巴子(给黑名单里面的key重置过期时间),这样后面恶意请求在过来,可以保证这个黑名单还有key的名字。直接让它面壁思过,给我唱征服。
1.2.设置Key为null
如果访问key的在数据库中也没有,就直接给给该key设置为NULL,并设置过期时间,毕竟null也需要占据空间的,还是得让它自己删除。这样后面请恶意请求直接让它与null谈话
面试官: 如果现在恶意请求兵分三路,分多波攻击,每次key都不一样。会怎么样呢?
如果恶意请求要打持久战,那流量肯定大,每次访问的都是不一样。
那首先肯定会有限流方案来限制这种级别的流量,以减少后端服务的冲击。所以到后端服务的流量整体规模就会变小,但总量可能还是会大于数据库承受压力。
如果请求key随机性和流量规模很大,相当于一个key访问一次就不来了,这样存储黑名单的列表就会很大。后面做查询时也就比较慢了。
例如用Redis-zset存储,流量规模一旦过大,它就需要很多的存储空间, 所以咱们可采用bitmap来减少存储空间和把Key分散开来。
如果请求key的随机性不是很大,流量规模大。但是这种类型请求很多,这时候还得看业务情况。
如果是个性业务,比如用户访问个人信息,这时得把key放到黑名单限制列表中,限制一下它那120迈的速度。
如果是共性业务,比如直接浏览的商品,那肯定得用队列来降降速。不然数据库真受不住。
1.3.第三方
那有没有更简单的方案,感觉上面很是繁琐? 其实,也有,你选择“布隆过滤器”这家伙就可以。
它是第Redis里面的第三方模块,使用时你自己需要做编译安装才可使用,不像Redis自带的常用数据类型。
每次访问缓存时,直接用布隆过滤器里面进行判断是否存在key,如果有,就去Redis里面读取,无,就拒绝请求。
相当于过滤请求的作用,而且布隆过滤器存储数据的单位是位(bit),所以整体占据的存储容量也不是很大,重要的是可以做到拦截请求操作。
但布隆过滤器自身具备不高误判率,随着存储的数量增多也会加大。所以布隆过滤器如何说找到了缓存那么是可能没有,如果找不到这个缓存那么就是一定没有缓存。
但这对广大的请求来说,几乎影响不大。
你这里是说请求访问key时就用布隆过滤器来判断,那刚开始没存储数据咋办,干看着它?
**不,不,不,你得这么来看 **
启动系统时,直接做缓存预热。就提前把热点数据或当前业务需要的缓存数据给载入到缓存中。这样后面请求来了,我发现你不是我自家兄弟的Key,直接妥妥拒绝没得商量。
可这热点数据、业务需要的数据要怎么发现呢?网站上线一开始肯定没运行啊,让布隆过滤器喝西北风?
。。。。。。我这个。
热点数据肯定是网站跑起来经过运行一段时间才能确定的,一般Redis不是会做持久化嘛,不然Redis-(RDB、AOF)吃干饭呀。直接根据持久化的数据恢复业务中需要的热点数据,一并更新到布隆过滤器。
虽然新系统、新业务没有遗留的历史数据,那我们可设置些规则,例如:最新、点击量高的数据给筛它一波。然后把满足这些规则的数据给设置到缓存和布隆过滤器中。
二、缓存上
这个比较直接,就是根据缓存的命中率来进行分析,如果我发现Redis内部某个key一直是未命中,而且访问次数还跳跃的很。那就直接上报给key的黑名单。
这样后面如果请求访问的key只要为它就直接干掉,相当于做一个黑key的发现,然后再来对它来做拦截
三眼再看你的简历,那 缓存雪崩 让我彻底沦陷
面试官: 那缓存雪崩要怎么解决?
吒吒辉: 缓存雪崩其实和缓存击穿有点类似,都是缓存失效。只不过它们各自针对缓存失效的情况不一样。
缓存雪崩是缓存同时大面积失效而导致请求像雪崩式的一拥而下,直捣黄龙(数据库)。而缓存击穿针对的是单个key访问恰巧失效的问题。
由此可见缓存雪崩的破坏力更大。关键要点在于同时会有大面积的key失效。那怎么解决呢?
一、业务上
设置缓存时,采用随机时间来把每个key的过期时刻都打乱,这样就不会统一集中失效,造成缓存雪崩。
是不是只要缓存大面积失效就会对后方造成问题?
这倒还不一定,如果你当前的子系统,不是什么流量大的业务,你会有问题吗?
比如:个人中心的基本资料,谁没事天天看,又不是不认识自己。
所以这问题还是得看业务特点,如果本身流量就比较高的业务在设计缓存时,就不要把缓存设置的那么集中。
比如:秒杀、热榜、首页的聚合数据等。这时就应该把它们的过期时间给错开掉
二、模式上
大流量的业务下,自然要缓存的数据也会很多。所以在设置缓存时,也可采用缓存数据分片来把缓存分配到不同的实例上来进行缓存,这样每个缓存实例的压力也会小很多。这样子就算失效了一部分它敢给我集体罢工嘛?
如果要保证多个缓存实例宕机后,整个系统还想继续麻溜的跑起来,那集群的方案自然是少不了的,有它才可以通过备节点快速补充上来。从而恢复业务的正常运行,抵御那些过高的网络请求。
总结
- 缓存高效于模式上的提速,内外HASH你可知道?
- 缓存穿透是请求访问数据库本不该出现的key,算恶意请求
- 业务上的规则检查防护
- 布隆过滤器
- 缓存设null
- 缓存击穿是请求访问key时恰巧失效,而打到数据库。
- 互斥锁、队列、分布式锁
- 缓存无失效
- 缓存雪崩是缓存出现大面积失效,而导致流量洪峰流到数据库
- 随机失效
- 集群处理
- 数据分片拆分
思考题
- 缓存三大问题的本质是什么造成的?
- 设计的方案很多,具体实践拿捏看什么?
- 你网站的热点数据发现规则会怎么考虑呢?要不要单独出业务?
如有感悟,欢迎关注。
点赞、分享、留言都可走一走。本来文章还是可以写很多东西,不过我感觉像微服务、优化、分布式等场景在这里不大合适,就准备单独出技术知识分享专题来做,不然大家就不好理解了。
也可以后面来一个高级版的缓存问题防御。这样大家可能很好理解。具体怎么安排大家可留言告知。除此之外以下相关的内容帮助大家提升
免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!