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

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

虽然这个业务不是我负责,但是也跟相关的人聊了下情况,感慨了一下,于是有了这一篇文章。

1.为何需要缓存?

在高并发请求时,为何我们频繁提到缓存技术?最直接的原因是,目前磁盘IO和网络IO相对于内存IO的成百上千倍的性能劣势。

做个简单计算,如果我们需要某个数据,该数据从数据库磁盘读出来需要0.1s,从交换机传过来需要0.05s,那么每个请求完成最少0.15s(当然,事实上磁盘和网络IO也没有这么慢,这里只是举例),该数据库服务器每秒只能响应67个请求;而如果该数据存在于本机内存里,读出来只需要10us,那么每秒钟能够响应100,000个请求。

通过将高频使用的数据存在离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的压力,不至于当场触发熔断了。