缓存大体可以分为三类:
客户端缓存;服务端缓存;网络中的缓存。
根据规模和部署方式缓存也可以分为:
单体缓存;缓存集群;分布式缓存。
可见,在软件系统中缓存几乎无处不在,所以说缓存为王不是没有原因的。
从浏览器到网络,再到应用服务器,甚至到数据库,通过在各个层面应用缓存技术,整个系统的性能将大幅提高。例如,缓存离客户端更近,从缓存请求内容比从源服务器所用时间更少,呈现速度更快,系统就显得更灵敏。缓存数据的重复使用,大大降低了用户的带宽使用,其实也是一种变相的省钱(如果流量要付费的话),同时保证了带宽请求在一个低水平上,更容易维护。所以,使用缓存技术,可以降低系统的响应时间,减少网络传输时间和应用延迟时间,进而提高了系统的吞吐量,增加了系统的并发用户数。利用缓存还可以最小化系统的工作量,使用了缓存就可以不必反复从数据源中查找,缓存所创建或提供的同一条数据更好地利用了系统的资源。
因此,缓存是系统调优时常用且行之有效的手段,无论是操作系统还是应用系统,缓存策略无处不在。
客户端缓存相对于其他端的缓存而言,要简单一些,而且通常是和服务端以及网络侧的应用或缓存配合使用的。对于互联网应用而言,也就是通常所说的BS架构应用,可以分为页面缓存和浏览器缓存。对于移动互联网应用而言,是指APP自身所使用的缓存。
页面缓存
页面缓存有两层含义:一个是页面自身对某些元素或全部元素进行缓存;另一层意思是服务端将静态页面或动态页面的元素进行缓存,然后给客户端使用。这里的页面缓存指的是页面自身的缓存或者离线应用缓存。
页面缓存是将之前渲染的页面保存为文件,当用户再次访问时可以避开网络连接,从而减少负载,提升性能和用户体验。随着单页面应用( Single Page Application,SPA)的广泛使用,加之HTML5支持了离线缓存和本地存储,大部分BS应用的页面缓存都可以举重若轻了。
HTML5提供的离线应用缓存机制,使得网页应用可以离线使用,这种机制在浏览器上支持度非常广,可以放心地使用该特性来加速页面的访问。
浏览器缓存
浏览器缓存是根据一套与服务器约定的规则进行工作的,工作规则很简单:检查以确保副本是最新的,通常只要一次会话。浏览器会在硬盘上专门开辟一个空间来存储资源副本作为缓存。在用户触发“后退”操作或点击一个之前看过的链接的时候,浏览器缓存会很管用。同样,如果访问系统中的同一张图片,该图片可以从浏览器缓存中调出并几乎立即显现出来。
对浏览器而言,HTTP1.0提供了一些很基本的缓存特性,例如在服务器侧设置 Expires的HTTP头来告诉客户端在重新请求文件之前缓存多久是安全的,可以通过if-modified-since 的条件请求来使用缓存。其中,发送的时间是文件最初被下载的时间,而不是即将过期的时间,如果文件没有改变,服务器可以用304-Not Modified 来应答。客户端收到304代码,就可以使用缓存的文件版本了。
HTTP 1.1有了较大的增强,缓存系统被形式化了,引入了实体标签e-tag。e-tag是文件或对象的唯一标识,这意味着可以请求一个资源,以及提供所持有的文件,然后询问服务器这个文件是否有变化。如果某一个文件的e-tag是有效的,那么服务器会生成304-Not Modified应答,并提供正确文件的e-tag,否则,发送200-OK应答。以 Web浏览器使用e-tag 为例。
在配置了Last-Modified/ETag的情况下,浏览器再次访问统一URI的资源时,还是会发送请求到服务器询问文件是否已经修改,如果没有,服务器会只发送一个304回给浏览器,浏览器则直接从本地缓存取数据;如果数据有变化,就将整个数据重新发给浏览器。
Last-Modified/ETag 与Cache-Control/Expires的作用是不一样的,如果检测到本地的缓存还在有效的时间范围内,浏览器则直接使用本地缓存,不会发送任何请求。两者一起使用时,Cache-Control/Expires 的优先级要高于Last-Modified/ETag。即当本地副本根据Cache-Control/Expires发现还在有效期内时,则不会再次发送请求去服务器询问修改时间(Last-Modified)或实体标识(e-tag)了。
Cache-Control与Expires的功能一致,都是指明当前资源的有效期,控制浏览器是直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于Expires。
一般情况下,使用Cache-Control/Expires 会配合Last-Modified/ETag一起使用,因为即使服务器设置缓存时间,当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时Last-Modified/ETag将能够很好利用服务端的返回码304,从而减少响应开销。
通过在HTML页面的节点中加人meta标签,可以告诉浏览器当前页面不被缓存,每次访问都需要去服务器拉取。
APP上的缓存
尽管混合编程( hybrid programming)成为时尚,但整个移动互联网目前还是原生应用(以下简称APP)的天下。无论大型或小型APP,灵活的缓存不仅大大减轻了服务器的压力,
而且因为更快速的用户体验而方便了用户。如何把APP缓存对于业务组件透明,以及APP缓存数据的及时更新,是APP缓存能否成功应用起来的关键。APP可以将内容缓存在内存、文件或本地数据库(例如SQLite)中,但基于内存的缓存要谨慎使用。
APP使用数据库缓存的方法:在下载完数据文件后,把文件的相关信息,如URL、路径、下载时间、过期时间等存放到数据库,下次下载的时候根据URL先从数据库中查询,如果查询到当前时间并未过期,就根据路径读取本地文件,从而实现缓存的效果。这种方法具有灵活存放文件的属性,进而提供了很大的扩展性,可以为其他的功能提供良好的支持。需要注意的是,要留心数据库缓存的清理机制。
对于APP中的某些界面,可以采用文件缓存的方法。这种方法使用文件操作的相关API得到文件的最后修改时间,与当前时间判断是否过期,从而实现缓存效果,操作简单,代价较低。需要注意的是,不同类型文件的缓存时间不一样。例如,图片文件的内容是相对不变的,直到最终被清理掉,APP可以永远读取缓存中的图片内容。而配置文件中的内容是可能更新的,需要设置一个可接受的缓存时间。同时,不同环境下的缓存时间标准也是不一样的,WiFi网络环境下,缓存时间可以设置短一点,一是网速较快,二是不产生流量费。而在移动数据流量环境下,缓存时间可以设置长一点,节省流量,而且用户体验也更好。
网络中的缓存位于客户端和服务端之间,代理或响应客户端的网络请求,从而对重复的请求返回缓存中的数据资源。同时,接受服务端的请求,更新缓存中的内容。
Web代理缓存
Web代理几乎是伴随着互联网诞生的,常用的 Web代理分为正向代理、反向代理和透明代理。Web代理缓存是将Web代理作为缓存的一种技术。
为了从源服务器取得内容,用户向代理服务器发送一个请求并指定目标服务器,然后代理服务向源服务器转交请求并将获得的内容返回给客户端。一般地,客户端要进行一些特别的设置才能使用正向代理。
反向代理与正向代理相反,对于客户端而言代理服务器就像是源服务器,并且客户端不需要进行设置。客户端向反向代理发送普通请求,接着反向代理将判断向何处转发请求,并将从源服务器获得的内容返回给客户端。
透明代理的意思是客户端根本不需要知道有代理服务器的存在,由代理服务器改变客户端请求的报文字段,并会传送真实的IP地址。加密的透明代理属于匿名代理,不用设置就可以使用代理了。透明代理的例子就是时下很多公司使用的行为管理软件。
Web正向代理代理缓存是指使用正向代理的缓存技术。Web代理缓存的作用跟浏览器的内置缓存类似,只是介于浏览器和互联网之间。
当通过代理服务器进行网络访问时,浏览器不是直接到Web服务器去取回网页而是向Web代理发出请求,由代理服务器来取回浏览器所需要的信息并传送给浏览器。而且,Web代理缓存有很大的存储空间,不断将新获取的数据储存到本地的存储器上,如果浏览器所请求的数据在Web代理的缓存上已经存在而且是最新的,那么就不重新从Web服务器取数据,而是直接将缓存的数据传送给用户的浏览器,这样就能显著提高浏览速度和效率。对于企业而言,使用Web代理既可以节省成本,又能提高性能。
对于Web代理缓存而言,较流行的是Squid,它支持建立复杂的缓存层级结构,拥有详细的日志、高性能缓存以及用户认证支持。Squid同时支持各种插件。
使用web反向代理服务器和使用正向代理服务器一样,可以拥有缓存的作用,反向代理缓存可以缓存原始资源服务器的资源,而不是每次都要向原始资源服务器请求数据,特别是一些静态的数据,比如图片和文件,很多Web服务器就具备反向代理的功能,比如大名鼎鼎的Nginx。
边缘缓存
如果反向代理服务器能够做到和用户来自同一个网络,那么用户访问反向代理服务器,就会得到很高质量的响应速度,所以可以将这样的反向代理缓存称为边缘缓存。边缘缓存在网络上位于靠近用户的一侧,可以处理来自不同用户的请求,主要用于向用户提供静态的内容,以减少应用服务器的介入。边缘缓存的一个有名的开源工具就是Varnish,在默认情况下进行保守缓存。也就是说,Varnish只缓存它所知的安全内容。Varnish 的一个特性是使用虚拟内存,精妙之处在于利用了操作系统的管理机制。Varnish可以高度定制如何处理请求,缓存哪些内容。
边缘缓存中典型的商业化服务就是CDN了,例如AWS 的 Cloud Front,我国的ChinaCache等,现在一般的公有云服务商都提供了CDN服务。CDN是Content Delivery Network 的简称,即“内容分发网络”的意思。。
服务端缓存是整个缓存体系中的重头戏,从网站的架构演进中已经看到了服务端缓存是系统性能的重中之重了。数据库是整个系统中的“慢性子”,有时候数据库调优能够以小搏大,在不改变架构和代码逻辑的前提下,缓存参数的调整往往是条捷径。在系统开发的过程中,可以直接在平台侧使用缓存框架,当缓存框架无法满足系统对性能的要求时,就需要在应用层自主开发应用级缓存了,即使利用可供参考的开源架构,应用级缓存的开发也是一件有挑战的事情。
数据库缓存
数据库属于IO密集型的应用,主要负责数据的管理及存储。数据库缓存是一类特殊的缓存,是数据库自身的缓存机制。大多数数据库不需要配置就可以快速运行,但并没有为特定的需求进行优化。在数据库调优的时候,缓存优化是一项很重要的工作。
当使用InnoDB存储引擎的时候,innodb_buffer_pool_size参数可能是影响性能的最为关键的一个参数了,用来设置用于缓存InnoDB索引及数据块的内存区域大小,更像是Oracle数据库的 db_cache_size。简单来说,当操作一个InnoDB表的时候,返回的所有数据或者查询过程中用到的任何一个索引块,都会在这个内存区域中去查询一遍。
和 key_buffer_size对于MyISAM引擎一样,innodb_buffer_pool_size 设置了InnoDB存储引擎需求最大的一块内存区域的大小,直接关系到InnoDB存储引擎的性能,所以如果有足够的内存,尽可将该参数设置到足够大,将尽可能多的InnoDB的索引及数据都放入到该缓存区域中,直至全部。
可以通过(Innodb_buffer_pool_read_requests - Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests*100%计算缓存命中率,并根据命中率来调整innodb_buffer_pool_size参数大小进行优化。
另外,table_cache是一个非常重要的MySQL性能参数,主要用于设置table高速缓存的数量。由于每个客户端连接都会至少访问一个表,因此该参数与max_connections有关。当某一连接访问一个表时,MySQL会检查当前已缓存表的数量。如果该表已经在缓存中打开,则会直接访问缓存中的表以加快查询速度;如果该表未被缓存,则会将当前的表添加进缓存并进行查询。在执行缓存操作之前,table_cache参数用于限制缓存表的最大数目:如果当前已经缓存的表未达到table_cache数目,则会将新表添加进来;若已经达到此值,MySQL将根据缓存表的最后查询时间、查询率等规则释放之前的缓存。
当然,深入数据库还有很多值得学习的地方,需要专业的技能,这就是很多公司专门设有DBA角色的原因。
应用级缓存
在系统开发的时候,适当地使用应用级缓存,往往可以取得事半功倍的效果。应用级缓存在这里指的是用来写带有缓存特性的应用框架,或者可用于缓存功能的专用库。
在Java语言中,缓存框架更多,例如 Ehcache,Cacheonix,Voldemort,JBoss Cache,oSCache 等等。
Ehcache是现在最流行的纯Java开源缓存框架,配置简单、结构清晰、功能强大,是从hibernate的缓存开始被广泛使用起来的。 EhCache有如下特点:
Voldemort是一款基于Java开发的分布式键-值缓存系统,像JBoss的缓存一样,Voldemort同样支持多台服务器之间的缓存同步,以增强系统的可靠性和读取性能。Voldemort 相当于是Amazon Dynamo的一个开源实现,LinkedIn用它解决了网站的高扩展性存储问题。
简单来说,就应用级缓存而言,只需要在框架侧配置一下属性即可,而不需要调用特定的方法或函数。系统中引入缓存技术往往就是从应用级缓存开始的,应用级缓存也通常会作为一级缓存使用。
平台级缓存
当应用级缓存不能满足系统性能要求的时候,就要考虑使用平台级缓存了。
平台级缓存,需要开发者通过代码来实现缓存机制。这里是NoSQL的胜场,不论是Redis还是 MongoDB,以及 Memcached都可以作为平台级缓存的重要技术。一种典型的方式是每分钟或一段时间后统一生成某类页面存储在缓存中,或者可以在热数据变化时更新缓存。
在实现缓存应用的时候,需要了解缓存技术中的几个术语。
缓存命中:当客户发起一个请求时,系统接收到这个请求,如果该请求的数据是在
缓存中,这一数据就会被使用,这一行为叫作缓存命中。
没有命中:cache miss是没有命中。如果缓存中还有存储空间,那么没有命中的对象会被存储到缓存中来。
存储成本:当没有缓存命中时,系统会从数据库或其他数据源取出数据,然后放人缓存。而把这个数据放入缓存所需要的时间和空间,就是存储成本。
缓存失效:当存储在缓存中的数据需要更新时,就意味着缓存中的这一数据失效了。替代策略:当缓存没有命中时,并且缓存容量已经满了,就需要在缓存中去除一条旧数据,然后加入一条新数据,而到底应该去除哪些数据,就是由替代策略决定的。
替代策略的具体实现就是缓存算法,这里简要介绍一下主流的缓存算法:
( 1 ):Least-Recently-Used (LRU)
替换掉最近被请求最少的对象,这种传统策略在实际中应用最广。在CPU缓存淘汰和虚拟内存系统中效果很好。然而在直接应用与代理缓存中效果欠佳,因为Web访问的时间局部性常常变化很大。
浏览器就一般使用了LRU作为缓存算法。新的对象会被放在缓存的顶部,当缓存达到了容量极限,底部的对象被去除,方法就是把最新被访问的缓存对象放到缓存池的顶部。
( 2 ) Least-Frequently-Used (LFU)
替换掉访问次数最少的缓存,这一策略意图是保留最常用的、最流行的对象,替换掉很少使用的那些数据。然而,有的文档可能有很高的使用频率,但之后再也不会用到。传统的LFU策略没有提供任何移除这类文件的机制,因此会导致“缓存污染”,即一个先前流行的缓存对象会在缓存中驻留很长时间,这样,就阻碍了新进来可能会流行的对象对它的替代。
( 3 ) Least Recently Used 2 (LRU2 )
LRU的变种,把被两次访问过的对象放入缓存池,当缓存池满了之后,会把有两次最少使用的缓存对象去除。因为需要跟踪对象2次,访问负载就会随着缓存池的增加而增加。
(4 ) Two Queues ( 2Q)
Two Queues是LRU的另一个变种,把被访问的数据放到LRU 的缓存中,如果这个对象再一次被访问,就把他转移到第二个、更大的LRU缓存,使用了多级缓存的方式。去除缓存对象是为了保持第一个缓存池是第二个缓存池的1/3。当缓存的访问负载是固定的时候,把LRU换成LRU2,就比增加缓存的容量更好。
(5 )SIZE
替换占用空间最大的对象,这一策略通过淘汰一个大对象而不是多个小对象来提高命中率。不过,可能有些进入缓存的小对象永远不会再被访问。SIZE 策略没有提供淘汰这类对象的机制,也会导致“缓存污染”。
( 6 )LRU-Threshold
不缓存超过某一size 的对象,其他与LRU相同。
( 7 ) Log(Size)+LRU
替换size最大的对象,当size相同时,按LRU进行替换。
( 8 ) Hyper-G
LFU的改进版,同时考虑上次访问时间和对象size。
(9 )Pitkow/Recker
替换最近最少使用的对象,除非所有对象都是今天访问过的。如果是这样,则替换掉最大的对象。这一策略试图符合每日访问Web网页的特定模式。这一策略也被建议在每天结束时运行,以释放被“旧的”、最近最少使用的对象占用的空间。
(10 ) Lowest-Latency-First
替换下载时间最少的文档。显然它的目标是最小化平均延迟。
(11 )Hybrid Hybrid
有一个目标是减少平均延迟。对缓存中的每个文档都会计算一个保留效用,保留效用最低的对象会被替换掉。位于服务器s 的文档f的效用函数定义如下:
(12 ) Lowest Relative Value (LRV)
LRV也是基于计算缓存中文档的保留效用,然后替换保留效用最低的文档。(13 ) Adaptive Replacement Cache (ARC)
ARC介于LRU和LFU之间,为了提高效果,由2个LRU组成,第一个包含的条目是最近只被使用过一次的,而第二个LRU包含的是最近被使用过两次的条目,因此,得到了新的对象和常用的对象。ARC能够自我调节,并且是低负载的。
( 14 )Most Recently Used (MRU)
MRU与LRU是相对,移除最近最多被使用的对象。当一次访问过来的时候,有些事情是无法预测的,并且在缓存系统中找出最少最近使用的对象是一项时间复杂度非常高的运算,这时会考虑 MRU,在数据库内存缓存中比较常见。
(15 ) First in First out ( FlFO)
FIFO通过一个队列去跟踪所有的缓存对象,最近最常用的缓存对象放在后面,而更早的缓存对象放在前面,当缓存容量满时,排在前面的缓存对象会被踢走,然后把新的缓存对象加进去。
( 16 ) Random Cache
随机缓存就是随意的替换缓存数据,比FIFO机制好,在某些情况下,甚至比LRU好,但是通常LRU都会比随机缓存更好些。
还有很多的缓存算法,例如Second Chance、Clock、Simple time-based、Extendedtime-based expiration、Sliding time-based expiration……各种缓存算法没有优劣之分,不同的实际应用场景,会用到不同的缓存算法。在实现缓存算法的时候,通常会考虑使用频率、获取成本、缓存容量和时间等因素。
一个使用了Redis集群和其他多种缓存技术的应用系统架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U2tWEc6d-1641307774921)(img/redis%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/image-20220104214532436.png)]
首先,用户的请会有一部分发送到CDN上,当然如果CDN中没有,有可能到企业内部去回源获取。另一部分请求被负载均衡服务分发到Nginx 上,此处常用的负载均衡算法是轮询或者一致性哈希,轮询可以使服务器的请求更加均衡,而一致性哈希可以提升Nginx应用的缓存命中率。
接着,Nginx应用服务器读取本地缓存,如果本地缓存命中则直接返回。Nginx应用服务器使用本地缓存可以提升整体的吞吐量,降低后端的压力,尤其应对热点数据的反复读取问题非常有效。
如果Nginx应用服务器的本地缓存没有命中,有两种选择,一种是由Nginx直接进一步读取相应的分布式缓存——Redis分布式缓存的集群,可以考虑使用主从架构或者Redis Cluster来提升性能和吞吐量,如果分布式缓存命中则直接返回相应数据,并回写到Nginx应用服务器的本地缓存中。
另一种则可以交给服务集群,服务集群可以首先读取本地应用缓存,如果应用缓存命中则直接返回数据,没有命中,依然可以读取Redis分布式缓存。
如果所有缓存都没有命中,系统就只能查询数据库或其他相关服务获取相关数据并返回,然后对本地缓存和分布式缓存进行更新,当然,我们已经知道数据库也是有缓存的。
整体来看,通过使用了多级缓存的系统,能保障系统具备优良的性能。