Web缓存是指一个Web资源(如html页面,图片,js,数据等)存在于Web服务器和客户端(浏览器)之间的副本。缓存会根据进来的请求保存输出内容的副本;当下一个请求来到的时候,如果是相同的URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。比较常见的就是浏览器会缓存访问过网站的网页,当再次访问这个URL地址的时候,如果网页没有更新,就不会再次下载网页,而是直接使用本地缓存的网页。只有当网站明确标识资源已经更新,浏览器才会再次下载网页。

web缓存的作用
减少网络带宽消耗(当Web缓存副本被使用时,只会产生极小的网络流量,可以有效的降低运营成本)
降低服务器压力(给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,间接降低服务器的压力。同时,搜索引擎的爬虫机器人也能根据过期机制降低爬取的频率,减小服务器压力)
减少网络延迟,加开页面打开速度

缓存机制是web开发的重要知识点,也是系统优化的重要方向。在前端开发中,缓存有利于加快网页的加载速度,同时缓存能够被反复利用,所以可以减少流量和带宽的开销。这里将系统的介绍在Web开发中的缓存方式,还会涉及到部分操作系统缓存知识。本文会重点介绍浏览器端的缓存机制也就是HTTP缓存,其机制是根据HTTP报文的缓存标识进行的。在分析缓存机制之前,先介绍下浏览器的HTTP报文。HTTP报文分为两种:

  • HTTP请求(Request)报文,报文格式为:请求行 – HTTP头(通用信息头,请求头,实体头) – 请求报文主体(只有POST才有报文主体),如下图


  • HTTP响应(Response)报文,报文格式为:状态行 – HTTP头(通用信息头,响应头,实体头) – 响应报文主体,如下图

注:通用信息头指的是请求和响应报文都支持的头域,分别为Cache-Control、Connection、Date、Pragma、Transfer-Encoding、Upgrade、Via;实体头则是实体信息的实体头域,分别为Allow、Content-Base、Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、Etag、Expires、Last-Modified、extension-header。这里只是为了方便理解,将通用信息头,响应头/请求头,实体头都归为了HTTP头。

Web开发中的不同缓存

1. 数据库缓存

我们可能听说过memcached,它就是一种数据库层面的缓存方案。类似的还有Redis缓存,也是一种基于内存的数据库,它的功能更加强大,并且支持备份和数据持久化。数据库缓存是指,当web应用的关系比较复杂,数据库中的表很多的时候,如果频繁进行 数据库查询,很容易导致数据库不堪重荷。为了提供查询的性能,将查询后的数据放到内存中进行缓存,下次查询时,直接从内存缓存直接返回,提供响应效率。

2. CDN缓存(服务端缓存)

CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能,可以归属为全局负载均衡或者说是四层负载均衡。通常情况下,浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。从浏览器角度来看,整个CDN就是一个源服务器,从这个层面来说,浏览器和服务器之间的缓存机制,在这种架构下同样适用。一般对于网站上的静态资源文件可以采用CDN分发,可以加快网站访问速度。

3. 代理服务器缓存(服务端缓存)

代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大,可以把它理解为一个共享缓存,不只为一个用户服务,一般为大量用户提供服务,因此在减少相应时间和带宽使用方面很有效,同一个副本会被重用多次。也可以将其归为七层负载均衡,但需要两次TCP握手连接,然功能更多,如会话保持,图片压缩,防盗链等。

4. 浏览器缓存

每个浏览器都实现了 HTTP 缓存,我们通过浏览器使用HTTP协议与服务器交互的时候,浏览器就会根据一套与服务器约定的规则进行缓存工作。最新的HTML5协议新增了离线缓存属性,对缓存机制进一步的优化,可以达到,实现图片存在客户端,跨域共享客户端缓存,做到真正的离线访问WEB应用,实现客户端的数据库。

5. 应用层缓存

应用层缓存是指我们在代码层面上做的缓存。通过代码逻辑,把曾经请求过的数据或资源等,缓存起来,可以根据实际情况选择将数据存在文件系统或者内存中,减少数据库查询或者读写瓶颈,提高响应效率,再次需要数据时通过逻辑上的处理选择可用的缓存的数据。h5新增的storage就可以归属到应用层缓存。

操作系统缓存

首先关于存储的概念有以下几种方式:
cache:缓存为了让从DB/磁盘拿出来的东西放到缓存(放于内存);
磁盘文件:本地存储的视频,图片,计算机里面的文件;
数据库:系统项目中的数据存储;
内存:计算机中所有的程序运行都是在内存中,所以内存对计算机的性能影响很大。
在操作系统缓存机制中,有buffer和cache两种方式,它们都是占用内存(buffer记录元数据,权限属性等,cache缓存文件)。I/O过程本身的延迟,以及高速设备与低速设备交互时的等待延迟,Buffer和Cache就是从这两个方向上产生的优化提高系统性能的方式。

buffer缓存是块设备的读写缓冲区,buffer是I/O缓存,用于内存和硬盘(或其他 I/O设备)之间的数据交换的速度而设计的。通常在写一个非常大的文件,文件会被分成一个个的小block块,往内存上写,然后再写入磁盘,这样的效率会很慢。这种情况下,内存就会攒足一次大的block块再写入磁盘,这样就不会有第一种情况里的延迟,这就是buffer。buffer的主要目的是进行流量整形,把突发的大数量较小规模读写整理成平稳的较大规模的I/O,以减少响应次数(比如从网上下载视频,不能下一点点数据就写入硬盘,而是达到一定量的数据一整块写,不然硬盘负荷太大)。

Cache缓存是高速缓存,用于cpu与内存之间的缓冲,是系统两端处理速度不匹配时的一种折衷策略。主要原因是cpu与memory,由于cpu快,memory跟不上,且有些值使用次数多,所以放入cache中,主要目的是使用内存来缓存可能被再次访问的数据,可以保持冗余的、被重复计算的、计算后的数据(buffer不行)。Cache是经常被使用在I/O请求上,来提高系统性能。如果cache的值很大,说明cache住的文件数很多。如果频繁访问到的文件都能被cache住,那么磁盘的读IO必会非常小。

Web中浏览器缓存

浏览器端的缓存分为强缓存(Expires和Cache-Control—优先级高,直接使用本地缓存资源)和协商缓存(Last-Modified / If-Modified-Since和 Etag / If-None-Match—优先级高,先发送请求到服务器端确认资源更改来确认是否使用缓存)。

1. 强缓存

在HTTP/1.0使用的是Expires字段,该字段表示缓存到期时间,即有效时间+当时服务器的时间,然后将这个时间设置在header中返回给服务器。如果发送请求的时间在expires之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源。该时间是一个绝对时间,由于服务端和浏览器端时间不一致,或者用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,因此更推荐另一种强缓存方式。Expires字段如下:

1
Expires: Thu, 15 Mar 2018 09:09:09 GMT

在HTTP/1.1中,增加了一个字段Cache-Control,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求,主要是利用该字段的max-age值来进行判断,它是一个相对值,资源第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则资源失效。

1
Cache-Control: max-age=9590000

Cache-Control除了max-age字段外,还有几个比较常用的字段设置值:

no-cache:不用本地缓存,使用协商缓存,先发送请求到服务器根据响应确认资源是否被更改,如果之前的响应中存在ETag或者Last-Modified,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。

no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。

public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。

private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。

注意:如果cache-control与expires同时存在的话,cache-control的优先级高于expires

2. 协商缓存

协商缓存(Last-Modified或者Etag)的过程是先从缓存中获取对应的数据标识,然后向服务器发送请求,确认数据是否更新,如果更新,则返回新数据和新缓存。反之,则返回304状态码,告知客户端缓存未更新,可继续使用,这正好弥补了一些强缓存的缺陷,协商缓存主要应用于一些时常需要动态更新的资源文件。协商缓存在协议里的字段是Last-Modified或者Etag,这两个字段都是成对出现的,即第一次请求的响应头带上某个字段(Last-Modified或者Etag),则后续请求则会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modified或者Etag字段,则请求头也不会有对应的字段。

Last-Modified/If-Modified-Since具体过程:

浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间

浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值

服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modified的header,因为既然资源没有变化,那么Last-Modified也就不会改变,这是服务器返回304时的response header

浏览器收到304的响应后,就会从缓存中加载资源

如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified的Header在重新加载的时候会被更新,下次请求时,If-Modified-Since会启用上次返回的Last-Modified值

Etag/If-None-Match

Etag字段值是由服务器生成的每个资源的唯一标识字符串(一般都是hash生成的),只要资源内容有变化这个值就会改变,其判断过程与Last-Modified类似,与它不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,响应头还会把这个ETag返回,即使这个ETag跟之前的没有变化。

Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,之所以在HTTP1.1中新增Etag主要为了解决几个Last-Modified的问题:

一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;

某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);

某些服务器不能精确的得到文件的最后修改时间。

这时,利用Etag能够更加准确的控制缓存,因为Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符。

注意:Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

用户浏览器行为对缓存的影响

刷新网页 => 如果缓存没有失效,浏览器会直接使用缓存;反之,则向服务器请求数据
手动刷新(F5) => 浏览器会认为缓存失效,在请求服务器时加上Cache-Control: max-age=0字段,然后询问服务器数据是否更新。
强制刷新(Ctrl + F5) => 浏览器会直接忽略缓存,在请求服务器时加上Cache-Control: no-cache字段,然后重新向服务器拉取文件。
更多用户行为可以用如下表格进行表示:

用户行为 Expires / Cache-Control Last-Modified / Etag
地址栏回车 有效 有效
页面链接跳转 有效 有效
新窗口 有效 有效
前进后退 有效 有效
F5刷新 无效 有效
Ctrl+F5刷新 无效 无效

移动端的缓存处理

Cache-Control和Last-Modified一般用在静态资源文件上,如JS、CSS和一些图像文件。通过设置资源文件缓存属性,对提高资源文件加载速度,节省流量很有意义,特别是移动网络环境。但问题是:缓存有效时长应该如何设置,如果太短,就起不到缓存的使用,设置的太长,在服务端资源文件有更新时,浏览器有缓存,则不能及时取到最新的文件。

对于移动端的缓存,任何一个网络请求的增加,加载消耗时间都是比较大的(尤其弱网环境下)。对于强缓存只要缓存不到期,是不会向服务器发送请求,但是如果是协商缓存的情况下,304的问题就比较大,它会造成无用的服务器请求,导致网络的延时。Last-Modified需要向服务器发起查询请求,才能知道资源文件有没有更新,虽然服务器可能返回304告诉没有更新,但也还有一个请求的过程。对于移动网络,这个请求可能是比较耗时的,有一种说法叫“消灭304”,指的就是优化掉304的请求。

通过抓包可以发现,带if-Modified-Since字段的请求,如果服务器回包304,回包会带有Cache-Control:max-age或Expires字段,文件的强缓存有效时间会更新,就是文件强缓存会重新有效。304回包后如果再请求,则又可以直接使用本地缓存文件了,不用再向服务器发送请求查询文件是否更新了,除非新的强缓存资源文件时间再次过期。

另外,Cache-Control与Last-Modified是浏览器内核的机制,一般都是标准的实现,不能更改或设置。以QQ浏览器的X5为例,Cache-Control与Last-Modified缓存不能禁用,缓存容量是12MB,不分Host,过期的缓存会最先被清除。如果都没过期,应该优先清最早的缓存或最快到期的或文件大小最大的,过期缓存也有可能还是有效的,清除缓存会导致资源文件的重新拉取。还有,浏览器,如X5,在使用缓存文件时,是没有对缓存文件内容进行校验的,这样缓存文件内容被修改的可能。

分析发现,浏览器的缓存机制还不是非常完美的缓存机制。完美的缓存机制应该是这样的:

缓存文件没更新,尽可能使用缓存,不用和服务器交互;
缓存文件有更新时,第一时间能使用到新的文件;
缓存的文件要保持完整性,不使用被修改过的缓存文件;
缓存的容量大小要能设置或控制,缓存文件不能因为存储空间限制或过期被清除。 以X5为例,第1、2条不能同时满足,第3、4条都不能满足。

在实际应用中,为了解决Cache-Control缓存时长不好设置的问题,以及为了“消灭304”,采用的方式是:

1. 在要缓存的资源文件名中加上版本号或文件MD5值字串,如common.d5d02a02.js、common.v1.js,同时设置Cache-Control:max-age=31536000,也就是一年。在一年时间内,资源文件如果本地有缓存,就会使用缓存,这样就可以避免协商缓存的304的回包现象。
2. 如果资源文件有修改,则更新文件内容,同时修改资源文件名,如common.v2.js,html页面也会引用新的资源文件名,实现静态资源非覆盖式更新。

通过这种方式,实现了:缓存文件没有更新,则使用缓存;缓存文件有更新,则第一时间使用最新文件的目的。即上面说的第1、2条。第3、4条由于浏览器内部机制,目前还无法满足。

浏览器缓存回忆

浏览器端强缓存优于协商缓存进行,先判断强缓存资源是否过期,若强缓存(Expires和Cache-Control)生效则直接使用本地浏览器缓存,若失效则进行协商缓存(Last-Modified / If-Modified-Since和 Etag / If-None-Match)发送请求到服务端,协商缓存由服务器响应和本地进行对比验证决定资源是否有效,若协商缓存失效,那么代表该请求的缓存失效,择重新发送请求获取响应结果下载资源,再存入浏览器缓存中,生效则返回304状态码,继续使用缓存,整个浏览器缓存主要过程如下图:

@转载请注明出处