高并发请求的缓存设计策略

前几天,我司出了个篓子。当时正值某喜闻乐见的关键比赛结束,一堆人打开我司app准备看点东西,结果历来没有感觉到过这么多关注量的该功能瞬间幸福到眩晕,触发了熔断,结果就是大量兴致冲冲打开app准备看该比赛结果的人被迫刷了十分钟三天前的野外跑酷,负责内容的人火大到直接骂娘。html

虽然这个业务不是我负责,可是也跟相关的人聊了下状况,感慨了一下,因而有了这一篇文章。web

1.为什么须要缓存?

在高并发请求时,为什么咱们频繁提到缓存技术?最直接的缘由是,目前磁盘IO和网络IO相对于内存IO的成百上千倍的性能劣势。
作个简单计算,若是咱们须要某个数据,该数据从数据库磁盘读出来须要0.1s,从交换机传过来须要0.05s,那么每一个请求完成最少0.15s(固然,事实上磁盘和网络IO也没有这么慢,这里只是举例),该数据库服务器每秒只能响应67个请求;而若是该数据存在于本机内存里,读出来只须要10us,那么每秒钟可以响应100,000个请求。redis

经过将高频使用的数据存在离cpu更近的位置,以减小数据传输时间,从而提升处理效率,这就是缓存的意义。数据库

2.在哪里用缓存?

一切地方。例如:浏览器

  • 咱们从硬盘读数据的时候,其实操做系统还额外把附近的数据都读到了内存里
  • 例如,CPU在从内存里读数据的时候,也额外读了许多数据到各级cache里
  • 各个输入输出之间用buffer保存一批数据统一发送和接受,而不是一个byte一个byte的处理

上面这是系统层面,在软件系统设计层面,不少地方也用了缓存:缓存

  • 浏览器会缓存页面的元素,这样在重复访问网页时,就避开了要从互联网上下载数据(例如大图片)
  • web服务会把静态的东西提早部署在CDN上,这也是一种缓存
  • 数据库会缓存查询,因此同一条查询第二次就是要比第一次快
  • 内存数据库(如redis)选择把大量数据存在内存而非硬盘里,这能够看做是一个大型缓存,只是把整个数据库缓存了起来
  • 应用程序把最近几回计算的结果放在本地内存里,若是下次到来的请求仍是原请求,就跳过计算直接返回结果

3.本次事故分析

回到本文开始的问题上,该系统是怎么设计的呢?底层是数据库,中间放了一层redis,前面的业务系统所需的数据都直接从redis里取,而后计算出结果返回给app;数据库和redis的同步另外有程序保证,避免redis的穿透,防止了程序里出现大量请求从redis里找不到,因而又一窝蜂的去查数据库,直接压垮数据库的状况。从这个角度讲,其实这一步是作的还能够的。服务器

可是这个系统有两个问题:
1.业务系统须要的数据虽然都在redis里,可是是分开存放的。什么意思呢,好比我前台发起一个请求,后台先去redis里取一下标题,而后再取一下做者,而后再取一下内容,再取一下评论,再取一下转发数等等……结果前台一次请求,后台要请求redis十几回。高并发的时候,压力一下被放大十几倍,redis响应、网络响应必然会变慢。
2.其实作业务的那波人也意识到了这个状况可能发生,因此作了熔断机制,另起了一个缓存池,里面放了一些备用数据,若是主业务超时,直接从缓存池里取数据返回。可是他们设计的时候没想周全,这个备选池的数据过时时间设计的太长了,里面竟然还有三天前更新进去的数据,最终致使了一大波用户刷出来三天前的野外生态小视频……网络

说到这,不知道读者有没有意识到他们最致命的一个问题:这个业务系统彻底没有考虑本地缓存(也就是在业务服务器内存里作缓存)。好比像咱们这种app,一旦大量用户同一时间涌进来,一定都是奔着少数几个内容去的,这种特别集中的高频次极少许数据访问,又不须要对每一个用户作特化的,简直就是在脸上写上“请缓存我”。
这时候,若是能在业务端作一层本地缓存,直接把算好的数据本地存一份,那么就会极大减小网络和redis的压力,不至于当场触发熔断了。并发

4.浅谈缓存的那些坑

缓存颇有用,可是缓存用很差也会埋不少坑:app

缓存穿透

缓存穿透是说收到了一个请求,可是该请求缓存里没有,只能去数据库里查询,而后放进缓存。这里面有两个风险,一个是同时有好多请求访问同一个数据,而后业务系统把这些请求全发到了数据库;第二个是有人恶意构造一个逻辑上不存在的数据,而后大量发送这个请求,这样每次请求都会被发送到数据库,可能致使数据挂掉。

怎么应对这种状况呢?对于恶意访问,一个思路是事先作校验,对恶意数据直接过滤掉,不要发到数据库层;第二个思路是缓存空结果,就是对查询不存在的数据仍然记录一条该数据不存在在缓存里,这样能有效的减小查询数据库的次数。

那么非恶意访问呢?这个要结合缓存击穿来说。

缓存击穿

上面提到的某个数据没有,而后好多请求都被发到数据库其实能够归为缓存击穿的范畴:对于热点数据,当数据失效的一瞬间,全部请求都被下放到数据库去请求更新缓存,数据库被压垮。

怎么防范这种问题呢?一个思路是全局锁,就是全部访问某个数据的请求都共享一个锁,得到锁的那个才有资格去访问数据库,其余线程必须等待。可是如今的业务都是分布式的,本地锁无法控制其余服务器也等待,因此要用到全局锁,好比用redis的setnx实现全局锁。

另外一个思路是对即将过时的数据主动刷新,作法能够有不少,好比起一个线程轮询数据,好比把全部数据划分为不一样的缓存区间,按期分区间刷新数据等等。这第二个思路又和咱们接下来要讲的缓存雪崩有关系。

缓存雪崩

缓存雪崩是指好比咱们给全部的数据设置了一样的过时时间,而后在某一个历史性时刻,整个缓存的数据所有过时了,而后瞬间全部的请求都被打到了数据库,数据库就崩了。

解决思路要么是分治,划分更小的缓存区间,按区间过时;要么是给每一个key的过时时间加个随机值,避免同时过时,达到错峰刷新缓存的目的。

缓存刷新

说到刷新缓存,其实也有坑的。好比我以前的一份工做里,有一次大活动,正是如火如荼的时候,全部的广告位忽然都变空白了。后来追查缘由,全部的广告素材都在缓存里,而后起了个程序,专门负责刷新缓存,每次把当前的素材全量刷新。

坏就坏在这个全量上。由于大活动的时候流量极大,广告更新压力也很大,把负责提供更新素材的程序压崩了。刷新缓存的程序在请求时,收到了一个返回结果Null。接下来就喜闻乐见了,刷新程序根据这个null,清空了整个缓存,全部广告素材都失效了。

总之,想要作好高并发系统的缓存,就要考虑到各类边角状况,当心设计,任何细小的疏忽均可能致使系统崩溃。

原文:https://www.cnblogs.com/bethunebtj/p/9159914.html

相关文章
相关标签/搜索