snowguy 2020-02-14
从学校出来,做开发工作也有一定时间了,最近有想系统地进一步深入学习,但发现基础知识不够扎实,故此来回顾基础知识,进一步巩固、加深印象。
最初开始接触编程时,总是自己跌跌撞撞、不断摸索地去学习,再一点点应用到实际项目中,知识点才更加清晰。后来,尝试写博客,把学到的知识试着分享出来,也是一次巩固的过程。
答:我了解的。目前电商首页以及热点数据都会去做缓存,一般缓存都是定时任务去刷新,或者是查不到之后去更新,定时任务刷新就有一个问题。
举个简单例子:如果所有首页的key失效时间都是12小时,中午12点刷新,我0点开始抢单活动,大量用户涌入,假设当时每秒6000个请求,本来缓存可以扛住每秒5000个请求,但是缓存当时所有的 Key 都失效了。此时,1秒 6000个请求全部打到数据库,数据库必然扛不住,它会报一下警,但实际情况可能是DBA都没反应过来数据库就直接挂了。此时,如果没有什么特别的方案来处理这个故障,DBA很着急,重启数据库,但是数据库马上又被新的流量打死了。这就是我理解的缓存雪崩。
同一时间缓存大面积失效,那一瞬间Redis跟没有一样,这个数量界别的请求直接打到数据库几乎是灾难性的。试想一下,如果打挂的是一个用户服务的库,那其他依赖它的库所有的接口几乎都会报错,如果没有做熔断等策略,基本上就是一瞬间挂一片的节奏,任你怎么重启用户都会把你打挂,等你重启好的时候,用户早睡觉去了,并且对我们的产品失去了信息。
答:处理缓存雪崩相对简单,在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好,这样就可以保证数据不会在同一时间大面积失效,我相信,Redis这点流量还是顶得住的。
setRedis( Key, value, time+Math.random() * 1000 );
如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效问题,不过在生产环境中操作集群的时候,单个服务都是对应的单个Redis分片,是为了方便数据的管理,但是也同样有可能回失效这样的弊端,失效时间随机是个好策略。
或者设置热点数据永远不过期,用更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就完事了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。
答:有了解的。先说说缓存穿透吧。缓存穿透是指缓存和数据库中都没有数据,而用户不断发起请求,我们数据库的 id 都是1开始自增上去的,如果发起 id = -1 的数据 或 id 为特别大(不存在)的数据,这是用户就很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。
小点的单机系统,可能用 postman 就能搞死,比如自己买的云服务。
像这种没有对参数进行校验,数据库 ID 都是大于0的,我们一直用小于 0 的参数去请求,每次都能绕开Redis直接打到数据库,数据库也查不到,每次都这样,并发高点就容易崩掉了。
至于缓存击穿嘛,这个和缓存雪崩有点像,但是又有点区别,缓存雪崩是因为大面积的缓存失效,打崩了DB,而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停地扛着大并发,大并发集中对一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿透缓存,直接请求数据库,就像一个完好无损的桶上凿开了一个洞。比如系统中某个商品,突然变成爆款,大量用户访问该商品,就可能发生缓存击穿。
答:缓存穿透是请求绕过缓存直接查数据库,我会在接口层增加校验,比如对用户权限校验,接口参数做校验,不合法的参数直接Return,比如 id 做基础校验,id<=0的直接拦截等等。
(这里我想补充一点,就是我们在开发的时候,要保持一颗“不信任”的心,就是不要相信任何调用方,比如我提供API接口出去,我需要这几个入参,那作为被调用方,任何可能的参数情况都应该被考虑到、并做校验,因为我不相信调用我接口的人,我不知道他会传什么参数给我。
举个简单例子,我这个接口是分页查询的,但是我没有对分页参数的大小做限制,调用的人万一 一口气查 Interger.MAX_VALUE 一次请求就需要我几秒,多几个并发我接口就挂了。是公司同事调用还好,大不了发现了改掉就是了,但是如果是黑客或者竞争对手,那问题就大了。在双十一或者抢单活动的时候调用这个接口,那损失就大了。)
从缓存取不到的数据,在数据库中也没有取到(不管是数据不存在,还是系统故障),这时也可以将对应Key的Value对写为null、“位置错误”、“稍后重试”这样的值(具体和产品沟通),或者看具体的场景,缓存的时间可以设置短点,如30秒(设置太长会导致正常情况下也没法适用),这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
这样可以防止攻击用户反复用同一个 id 暴力攻击,但是我们要知道正常用户是不会在单秒内发起这么多次请求的,那网关层Nginx 我们也可以添加配置项,可以让运维人员对单个 IP 每秒访问次数超出阀值的 IP 都拉黑。
答:记得Redis还有一个高级用法布隆过滤器(Bloom Filter),这个也能很好地防止缓存穿透的发生,他的原理也很简单,就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你Return就好了,存在你就去查了DB刷新Key、Value,再Return。
缓存击穿的话,设置热点数据永远不过期,或者加上互斥锁就能搞定了。(代码如下)
/** * 获取数据 * @param key 查询参数 * @return data 数据 * @throws InterruptedException 异常 * @author 柠檬先生 */ public static String getData(String key) throws InterruptedException { // 从redis查询数据 String result = getDataFromKV(key); // 校验数据 if (StringUtils.isBlank(result)){ // 获取锁 if (reenLock.tryLock()) { // 去数据库查询 result = getDataFromDB(key); // 校验 if (StringUtils.isNotBlank(result)){ // 保存到缓存中; setDataToKV(key, result); } // *释放锁 正常会再finally里面释放; reenLock.unLock(); }else { // 睡一会儿再拿 Thread.sleep(100L); result = getData(key); } } return result; } //这里的锁是单机版玩法.,分布式锁还是得靠lua脚本这样的;
以上介绍了Redis的雪崩、击穿、穿透,三者其实都差不多,但是又有些区别,在面试中这是问到缓存必问的,因为缓存的雪崩、击穿、穿透,是缓存最大的问题,要么不出现,一旦出现就是致命性的问题,所以面试官一定会问。
我们学习的时候一定要理解是怎么发生,以及怎么去避免的,发生之后怎么去抢救,你可以不是了解很深入,但是你一定不能什么都不去想,面试有时候不一定是对只是面的拷问,或许是对你态度的拷问,如果你思路清晰,然后知其然还能知其所以然那就更棒了,还能知道怎么预防,那offer就是手到擒来了。
一般避免以上情况发生我们要从3个时间段去分析:
上面提到的限流组件,可以设置每秒的请求,有多少能通过组件,剩余未通过的请求,怎么办?走降级!可以返回一些默认值,或者友情提示,或者空白的值。
这样做的好处是:数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。只要数据库不死,对用户来说, 3/5 的请求都是可以被处理的。只要有 3/5 的请求可以被处理,就意味着系统没有死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次就可以刷出来一次。
这个在目前主流的互联网大厂里面是最常见的,以前遇到过,某明星爆出什么事情,会发现微博怎么刷都是空白界面,但是有的人有直接进去了,多刷几次又出来了,这就是做了降级,牺牲部分用户体验来换取服务器的安全,还行。
还有就是今年(2019)的双十一,走的就是“限流、降级”,下单接口其实没有挂,牺牲部分用户体验,保住服务器,你多点几下是可以成功的,等流量高峰过去了,所有的用户全部都恢复正常访问,服务器啥事也没有。
去年(2018)退款接口被打崩了,今年阿里也聪明了很多,退款接口在12日0点开放,这就有效避开了流量高峰。