weijunfeng
3/6/2020 - 7:14 AM

HTTP缓存

HTTP缓存
Arvin飞
Arvin飞
​
上海汉得信息技术股份有限公司 前端工程师
5 人赞同了该文章
重用已获取的资源能够有效的提升网站与应用的性能。Web 缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间。借助 HTTP 缓存,Web 站点变得更具有响应性。

各种各样类型的缓存
缓存是什么?简单来说:缓存是一种将你浏览过的资源给保存下来,在你下次打开的时候直接调用(省掉向服务器发送请求,并将请求展示出来的时间),并展示出来。在http的官网上,对缓存的定义是:一种保存资源副本并在下次请求时直接使用该副本的技术。当web端发现需要请求的资源已经被缓存下来,那么它会拦截请求,返回该资源的缓存(拷贝),而不会去服务器重新下载。这样带来的好处有很多:可以缓解服务端的压力,提升性能(获取资源的耗时更短,并且重复请求的频率更低了)。对于网站来说,适当的缓存是达到一个高性能的重要组成部分。不过呢,缓存需要合理的配置,并不是所有的资源都是永久不变的。我们在使用缓存是应保有一个这样的概念:缓存并不是说永久不变的,我们对一个资源的缓存应该截止到其下一次发生改变(即不能缓存过期的资源)。

缓存的种类有很多,其大致可归为两类:私有缓存和公有缓存。公有缓存很好理解,就是大家都可以用的缓存,其存储的响应能够被多个用户使用。私有缓存只能够被单独的用户使用。

https://mdn.mozillademos.org/files/13777/HTTPCachtType.png
​
mdn.mozillademos.org
(私有)浏览器缓存:

私有缓存只能用于单独的用户。比较常见的是浏览器中设置的“缓存”选项。浏览器缓存拥有用户通过HTTP或HTTPS下载的所有文档。这些缓存的作用就是为浏览过的文档提供导航、保存网页、查看源代码等功能,可以避免再次向服务器发起多余的请求。同时它也可以提供缓存内容的离线浏览。

(公有)浏览器缓存:

公有的浏览器缓存,也叫做共享缓存可以被多个用户使用。这个不多见,主要是通过代理的方式实现的。例如:ISP或者你所在的公司会架设一个web代理作为本地基础网络的一部分提供给不同的用户。像这样热门的资源被重复的利用,能够有效的减少网络的拥堵和延迟。

缓存所操作的目标
虽然HTTP缓存不是必须的,但是重(chong)用缓存的资源却是必要的。这能保证我们的网站能够高效的运转。常见的HTTP缓存只能存储GET响应,对于其他类型的响应却是心有余而力不足。缓存的关键主要包括request method和目标URL。一般只有GET请求才会被缓存。普遍的缓存案例及其响应状态码有很多:例如200、301、404、403、500等等,具体可以看我个人网站的一篇文章。

各种状态码及其包含的意义 - Avrin、飞的博客
​
www.flygoing.cn
图标
针对一些特定的请求,也可以通过关键字区分多个存储的不同响应以组成缓存的内容。具体参考下文有关Vary的信息。

缓存控制
Cache-control 头
HTTP/1.1定义的Cache-Control头用来区分对缓存机制的支持响应情况,请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。

禁止进行缓存

缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。

1. Cache-Control:no-store
2. Cache-Control:no-cache,no-store,must-revalidate
强制确认缓存

如下的头部定义,在这种方式下,每次有请求发出时,缓存会将此请求发到服务器(该请求带有与本地缓存相关的验证字段),服务器端会验证请求中所描述的缓存是否过期,若未过期(注:实际上返回的就是304),则缓存才使用本地缓存的资源副本。

1. Cache-Control:no-cache
私有缓存和公有缓存

“public”指令表示该响应可以被任何中间层(中间代理、CND等)缓存。如果指定了“public”,则一些通常不被中间层缓存的页面(默认是private)例如:带有HTTP验证信息(账号、密码、手机号等)的页面或者某些特定能够影响状态码(重定向)的页面,将会被缓存。

而“private”则表示该响应是专用于某单个用户的,中间人不能缓存此响应,该响应只能应用于浏览器的私有缓存中。

1. Cache-Control:private
2. Cache-Control:public
缓存过期机制

过期机制中,最重要的指令是 "max-age=<seconds>",表示资源能够被缓存(保持活性)的最大时间。相对Expires而言,max-age是距离请求发起的时间的秒数。针对应用中那些不会改变的文件,通常可以手动设置一定的时长以保证缓存有效,例如图片、css、js等静态资源。

详情看以下关于缓存有效性的内容

1. Cache-Control : max-age=31536000
缓存验证确认

当使用了“must-revalidate”指令,那就意味着缓存在考虑使用一个陈旧(可能已经过期)的资源时,必须先验证它的状态,已过期的缓存,将不会被使用。详情看关于缓存校验的内容。

1. Cache-Control : must-revalidate
Pragma 头
Pragma 是HTTP/1.0标准中定义的一个header属性,请求中包含Pragma的效果跟在头信息中定义Cache-Control:no-cache相同,但是HTTP的响应头不支持该属性,所以它不能拿来完全代替HTTP/1.1中定义的Cache-Control头。通常定义Pragma的目的是:向后兼容基于HTTP/1.0的客户端。

Freshness(新鲜度)

理论上来说,当一个资源被缓存存储后,该资源应该可以被永久存储在缓存中。由于缓存只有有限的空间来存储资源的副本,所以缓存会定期将一些不必要的或者过期的副本删除,这个过程学名叫做缓存驱逐。另一方面,当服务器上的资源进行了更新,那么缓存中的对应资源也应该被更新,由于HTTP是C/S模式的协议,服务器更新一个资源时,不可能直接通知客户端及其缓存,所以双方必须为该资源约定一个过期时间,在该过期时间之前,就认为该资源(缓存的副本)就是最新的,当过了过期时间后,该资源(缓存副本)就会变成陈旧的。驱逐算法的作用就是将陈旧的资源(缓存副本)替换成新鲜的。PS:一个陈旧的缓存副本是不会直接清除或忽略的,当客户端发起一个请求时,缓存会先检索对应资源是否是陈旧的资源,当检索到已有一个对应的陈旧资源(缓存副本)时,缓存会先将此请求附加一个If-None-Match头,然后发给目标服务器,若服务器返回了304(Not Modified)(该响应不会带有实体信息),则表示此资源副本是新鲜的,还没有过期的,这样,因为只返回个响应头,不带有实体信息,可以节省一些带宽和服务器压力。如果服务器通过If-None-Match或者If-Modified-Since判断后发现已过期,那么就会带有该资源的实体内容返回。

下面是一个上述缓存处理过程的图示:


简而言之就是:第一次请求,通过cache中间层,到达Server服务层,请求到实体资源,返回给前端,同时设置(也可以认为是双方约定)缓存的过期时间是100秒,然后第二次去请求,因为之前有设置Cache-Control为100秒,在低于100秒时,去请求,直接在cache中间层验证后,发现缓存资源副本还是新鲜的,即返回304,同时返回一个age字段,以表明是在第一次请求后何时再次请求的。在100秒之后,进行第三次请求,cache中间层进行判断,发现其已过期,故而将请求传递给Server层,将实体资源返回。

对于含有特定头信息的请求,会去计算缓存寿命。比如Cache-control: max-age=N的请求头,相应的缓存的寿命就是N。通常情况下,对于不含这个属性的请求则会去查看是否包含Expires属性,通过比较Expires的值和头里面Date属性的值来判断是否缓存还有效。如果max-age和expires属性都没有,找找头里的Last-Modified信息。如果有,缓存的寿命就等于头里面Date的值减去Last-Modified的值除以10(注:根据rfc2626其实也就是乘以10%)。

缓存失效时间的计算公式如下:

expirationTime = responseTime + freshnessLifetime - currentAge
上式中,responseTime表示浏览器接收到此响应的时间点。

加速资源
更多的利用缓存资源,能够有效的提高网站的性能和响应速度。同时,为了优化缓存,防止无用的垃圾缓存大量的占用内存,过期时间设置得尽量长是一种很好的应对方法。对于定期或者频繁更新资源,如此做法是比较稳妥的,但是对于那些长期不更新的资源会有些问题。这些固定的资源在一定时间内受益于这种长期保持的缓存策略,但是如果一旦进行更新就会很困难。这里特指网页上引入的一些js/css文件,当它们变动时需要尽快更新线上资源。

为了更好的优化缓存,提高网站的性能和响应速度,web开发者发明了一种Steve Sounders,称作加速(revving)的技术。不频繁更新的文件会使用特定的命名方式:在URL后面(通常呢是在文件名后面)会添加上版本号。

加上版本号的资源被视为一个完全新的独立的资源,同时拥有一年甚至更长的缓存过期时长。但是也存在一个问题,所有引用此资源的地方都需要更新链接。web开发者们通常会采用自动化构建工具在实际工作中完成这些琐碎的工作。当低频更新的资源(js/css)变动了,只用在高频变动的资源文件(html)里做入口的改动。

这种方法还有个好处:同时更新两个缓存资源不会造成部分缓存先更新,从而引起新旧文件内容不一致的问题。对于互相有依赖关系的css和js文件,避免这种不一致的情况是非常重要的。


加在加速文件后面的版本号不一定是一个正式的版本号字符串,如1.1.1或者其他固定自增的版本数。它存在的意义是防止缓存碰撞,它可以是任何标记如hash或者时间戳。

缓存验证
用户点击刷新按钮时会开始缓存验证。如果缓存的响应头信息里含有"Cache-control:must-revalidate"的定义,在浏览的过程中也会触发缓存验证。另外,在浏览器偏好设置里设置Advanced->Cache为强制验证缓存也能达到相同的效果。

当缓存的文档过期后,需要进行缓存验证或者重新获取资源。只有在服务器返回强校验或者弱校验器时才会进行校验。

ETags

这个作为缓存的一种强校验器,ETag 响应头是一个对用户代理(User Agent, 下面简称UA)不透明(译者注:UA无需理解,只需要按规定使用即可)的值。对于像浏览器这样的HTTP UA,不知道ETag代表什么,不能预测它的值是多少。如果资源请求的响应头里含有ETag, 客户端可以在后续的请求的头中带上 If-None-Match 头来验证缓存。

Last-Modified 响应头可以作为一种弱校验器。弱的原因在于它只能精确到一秒。如果响应头里含有此信息,客户端可以在后续的请求中带上If-Modified-Since 来验证缓存。

当向服务器发起缓存校验的请求时,服务端返回200 ok表示返回正常的结果或者304Not Modified(不返回body)表示浏览器可以使用本地的缓存。304的响应头也可以同时更新缓存文档的过期时间。

带Vary头的响应
Vary HTTP 响应头决定了对于后续的请求头,如何判断是请求一个新的资源还是使用缓存的文件。

当缓存服务器收到一个请求,只有当前的请求和原始(缓存)的请求头跟缓存的响应头里的Vary全部是匹配的,才能使用缓存的响应。


使用vary头有利于内容服务的动态多样性。例如:

使用Vary: User-Agent头,缓存服务器需要通过UA判断是否使用缓存的页面。如果需要区分移动端和桌面端的展示内容,利用这种方式就能避免在不同的终端展示错误的布局。另外,它可以帮助google或者其他搜索引擎更好地发现页面的移动版本,并且告诉搜索引擎没有引入Cloaking。

1. Vary: User-Agent
因为移动版和桌面的客户端的请求头中的User-Agent不同,缓存服务器不会错误地把移动端的内容输出到桌面端到用户。