从代码说起 fn longRunningOperations(){ ... // 很耗时}let result = longRunningOperations();// do other thing 我们来看上面这段伪代码,longRunningOperations是个很耗时的方法(调用一次要几十秒甚至几分钟),比如:
对于这个方法,如果每次都去调用一次的话,会非常的影响性能,用户体验也非常的不好。 那我们该如何处理呢? 一般有几种优化方案:
本文主要聊聊第三种方案:使用「缓存」! 主动缓存与被动缓存 一般我们使用缓存来存储一些内容,这些内容有如下一些特点(符合一条或多条):
比如,
对于字典数据来说,一般我们的做法是在系统启动时,将字典数据直接加载到缓存中,此类缓存数据一般没有过期时间;当修改字典时,会同时更新缓存中的内容。此类缓存称为「主动缓存」,因为其缓存数据是由用户的主动修改来触发更新的。 而对于某些信息来说,因为信息量太大,不能一次性全部加载到缓存中,且也不是太清楚哪些数据访问频次高、哪些数据访问频次低。对于这样的数据,一般的做法是:
此类缓存称为「被动缓存」!其缓存的数据的过期由系统来控制。那系统如何控制呢?这就涉及到缓存置换算法! 缓存置换算法 上面说了,对于被动缓存来说,由于信息量太大,数据不能一次全部加载到缓存中,当缓存满了以后,需要新增数据时,就需要确定哪些数据要从缓存里清除,给新数据腾出空间。 用于判断哪些数据优先从缓存中剔除的算法称为「缓存(页面)置换算法」! Wiki中列出了如下置换算法:
一般情况下我们不会自己去实现个缓存,市面上有不少开源的缓存中间件,比如:redis,memcached。这里只简单的梳理几个常用的置换算法。 FIFO FIFO应该算是最简单的置换算法了:
FIFO的实现很简单,但是其性能并不总是很好。举个简单的例子,假设一个系统需要10个缓存数据,恰巧此时5个数据在队列头部,另外5个数据不在缓存中,又恰巧此时队列又满了。按照FIFO算法,5条不在内存中的数据被加载到了缓存中,而之前的5条数据被清除了。这就需要再次将被清除的5条数据加载到缓存中。这就影响了性能。 这个问题可能会随着所分配的缓存大小的增加而增加,原本我们使用缓存是为了提高性能的,现在可能会影响性能,这种现象称为「Belady现象」! LIFO和FIFO很类似,这里就不赘述了。 LRU 目前比较常用的置换算法称为LRU置换算法:优先替换掉「最近最少使用」的数据
LRU的变体有很多,例如:
还有和LRU类似的MRU,LFU这里不在赘述! 缓存集群 为了提高缓存的可用性,一般我们至少会对缓存做个主备,即一个主缓存,一个从缓存。
再安全一点的做法就是做缓存集群:
分布式缓存 无论是单机缓存,主从备份还是缓存集群,都没法解决缓存大小限制的问题。因为一般缓存会使用内存,而一台机器的内存大小是有限的。当需要缓存的数据远远超过一台机器的内存大小的时候,就需要将缓存的数据分布到多台机器上。每台机器只缓存一部分数据,这就是分布式缓存。 分布式缓存可以解决一台机器缓存数据有限的问题,但是也引入了新的问题:
一般做法是对key进行hash,然后对服务器数量进行取余,来确定数据在哪台服务器上。这解决了「哪些数据该缓存在哪台服务器上」的问题,但是却无法保证「每台服务器缓存的数据量基本相同」,因为可能多个key的hash取余后都落到了同一个服务器上,这就可能导致其中一台服务器缓存的数量很多,其它服务器缓存的数据量很少。缓存数据量多的服务器可能会内存不够用,触发数据置换,进而导致性能下降。 可以使用一致性hash环来保证服务器缓存的数据量基本相同,大致逻辑如下:
无处不在的缓存 上面聊的主要是应用缓存,实际上,缓存无处不在。 下面通过我们访问网站的流程,来简单梳理一下,整个过程中,哪些地方可能会用到缓存。 网络缓存 当我们在浏览器中输入URL,按下回车后。 首先,需要查找域名所对应的IP!这里就有各种缓存!
找到IP后,还不一定要发请求,因为你访问的资源可能之前已经访问过,已经被缓存到了浏览器缓存中。此时,浏览器直接返回缓存,而不会发送请求。 如果没有缓存,则发送请求获取资源。 后面可能会达到CDN。CDN是一种边缘缓存。在用户访问网站时,利用GSLB(Global Server Load Balance,全局负载均衡)技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。如果CDN中找不到需要的资源,则请求可能就到了反向代理。 某些反向代理能够做到和用户来自同一个网络,那么用户访问反向代理服务器的时候,就会得到很高质量的响应速度,这样的反向代理缓存一般称为边缘缓存,而CDN在边缘缓存的基础上,使用了GSLB 一般反向代理有两个功能:
如果反向代理中也找不到需要的资源,请求才到达源服务器来获取资源。 服务端与数据库缓存 一般情况下,Server接收到请求后,会根据请求,组装出响应,进行返回。这个过程可能需要查询数据库、进行业务逻辑计算、页面渲染等操作。这里的每一步都可以引入缓存。 对于数据库查询来说,目前一般的持久化框架都会提供查询缓存。即对于相同的sql,第二次查询开始,可以不用再查询数据库,直接从缓存中获取第一次查询所返回的数据。节省了调用数据库查询的时间消耗。对于某些访问量很大的数据,也可以将其缓存到缓存中间件中。后续直接从缓存中间件中获取。 而数据库本身也有缓存!
mysql的查询缓存可能会降低效率。首先,写缓存是独占模式写入。其次,假设一个查询结果被缓存了,当涉及到的其中一张表数据更新,该缓存都会被置为无效。对于频繁修改的数据,使用缓存就会降低效率。 对于业务逻辑计算来说,如果某些业务逻辑很复杂,那么可以针对结果进行缓存。可以将结果缓存到数据库或缓存中间件中。对于相同的参数的请求,第二次请求时,就不必进行计算,直接从缓存中返回结果即可。 对于页面渲染来说,某些访问量很大的页面,且数据基本不变的情况下,可以对页面进行静态化。即生成静态的页面,不必每次访问的时候都动态生成页面进行返回,而是预先生成好页面,将其存到磁盘上,当访问该页面的时候,直接从磁盘获取页面进行返回即可。或者直接将页面内容缓存到缓存中间件中,进一步提高性能。 另外,对于需要登录的Server来说,用户信息其实也是缓存下来的。不论是存到服务器Session中,还是存到了缓存中间件中。否则,每次用户访问Server都需要到数据库获取用户信息,会影响Server端性能! 计算机缓存 最后,运行系统的计算机本身也有很多的缓存! 我们都知道,一般计算机由CPU、内存、主板、硬盘、显卡、显示器、鼠标、键盘、网卡等组成!其中存储类设备包括了:云存储(例如:百度云盘,NAS等)、本地硬盘、内存、CPU中的高速缓存(我们常说的一级缓存、二级缓存和三级缓存)以及CPU寄存器。它们的速度各异,差异达数个量级。下图显示了各个设备的访问速率。 
我们都知道CPU的高速缓存是「缓存」,实际上上面的设备,上层设备都可以说是下层设备的「缓存」! 在《深入理解计算机》一书中,简单的介绍了计算机执行C语言的hello world程序时的计算机流程。
可以看到,绝大部分的操作,都是数据的拷贝!最终被CPU执行,为了数据能更快的到达CPU,就有了一层一层的「缓存」!
总结 性能是架构设计时需要着重考虑的一个非功能性约束,而引入缓存是提高系统性能的一个简单且直接的方法。 本文从一个简单的伪代码开始,简单阐述了,缓存的作用,涉及的技术以及目前缓存的使用场景,以期能对架构设计提供一些参考。 END