缓存几乎是计算机科学当中最常用的性能优化的方法了。CPU的设计里有一级二级高速缓存,数据库里也有缓存,Java也有像redis之类的缓存数据库,几乎所有的IO操作,网络请求等所有耗时操作,在考虑性能优化时都会考虑缓存,当然HTTP也不例外…

关于HTTP缓存的文章网上实在是五花八门的,各种大佬已经写过了各式各样的总结,我看起来也是乱如麻,模模糊糊,云里雾里。一来是HTTP缓存实在是涉及太多知识点了,二来还有一些1.0版本的缓存策略,在1.1版本时虽然提出了新的策略但是因为要兼容1.0所以也不得不把1.0的也包含进来。因此更显复杂。

好吧吐槽结束,我来稍微简略理一理HTTP缓存的一些主要知识,入个门就好,不求深入。

首先缓存的种类有很多,什么代理服务器缓存、客户端缓存、网关缓存、CDN、反向代理缓存…我们现在只讨论代理缓存和客户端缓存

  • 代理服务器缓存

代理缓存是一种共享缓存可以被多个用户使用。缓存存在代理服务器的磁盘里,例如,ISP 或你所在的公司可能会架设一个 web 代理来作为本地网络基础的一部分提供给用户。这样热门的资源就会被重复使用,减少网络拥堵与延迟。

  • 客户端缓存

客户端缓存属于私有缓存只能用于单独用户。缓存存在用户的本地磁盘里。浏览器缓存拥有用户通过 HTTP 下载的所有文档。这些缓存为浏览过的文档提供向后/向前导航,保存网页,查看源码等功能,可以避免再次向服务器发起多余的请求。它同样可以提供缓存内容的离线浏览。

我个人的观点是二者只是缓存的位置以及可访问的人不同,完全可以把客户端缓存当成是一个私人的缓存代理服务器,因此下文的缓存代理服务器包含了以上二者

Cache-Control

HTTP/1.1之前的版本(HTTP1.0、0.9)在缓存控制时使用Pragma这个首部,而新的HTTP/1.1则改用了Cache-Control。但是因为无法保证传输过程中所有的中间服务器都以HTTP/1.1为基准,所以发送的请求常常会含有两个首部字段

Cache-Control: no-cache
PragmaL no-cache(如果遇到老旧的服务器,上面失效,这行生效)

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

public/private(响应报文)

Cache-Control: public/private(默认值)

public的话则表示该响应的主体缓存既可以存在代理服务器也可以存在客户端浏览器。并且是完全公开的,不是发送该请求的用户也可以利用这个缓存。

private表示该响应的主体只能由发送该请求的用户存在他的用户浏览器里或者是存在缓存代理服务器那里,不过只有发送该请求的用户才可以从缓存代理服务器那里获得该份缓存。

no-cache/no-store

Cache-Control: no-cache

如果是客户端发送请求包含no-cache,则表示客户端不会接收缓存过期的资源。于是,缓存代理服务器(或者浏览器也?)会根据这个请求的首部中带有的If-None-Match、If-Modified-Since之类的验证字段向源服务器发送一个验证请求,如果用户的缓存还没有过期,则会返回304(不带资源主体),,然后允许用户继续使用这个缓存,如果过期了就会返回200,并且带着资源主体。(这个验证的过程叫做新鲜度检验)

如果是服务端返回的响应带着no-cache,则缓存服务器和客户端仍然能对资源进行缓存,只不过指定了Max-Age:0 。也就是说这个缓存会马上过期。当用户请求缓存代理服务器上的过期缓存时,那就会向源服务器发起新鲜度检验。

Cache-Control: no-store

no-store是真正的不进行缓存,缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。

max-age

Cache-Control: max-age=604800 (单位:秒)

客户端发送请求包含max-age时,如果判定缓存资源的缓存时间数值比max-age小,那么客户端就接收缓存资源。如果max-age=0的话,意思就是缓存代理服务器要对源服务器进行新鲜度检验。

当服务器返回的响应包含max-age指令时,缓存服务器将不对资源有效性再做确认,max-age的数值代表资源保存为缓存的最长时间。

must-revalidate

Cache-Control: must-revalidate

使用了这个的话,缓存代理服务器(也包括浏览器)就会向源服务器发起新鲜度检验。

MDN的精彩论述


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

有关ETags
是在响应报文里的

ETag: "82e22293907ce725faf6777395acd12"

Etag基本上可以理解为服务器为一个资源,假设为a.txt贴上一个标签假设为usagi-1234。这个文件被缓存代理服务器缓存了,后来服务器更新了a.txt.在更新的同时也更新Etag值,比如改为usafi-5678。之后缓存代理服务器发来请求If-None-Match:usagi-1234。服务端发现与现在服务器上的a.txt的Etag值不同,就会返回200而不是304.这样缓存代理服务器上的就缓存版本就会被新版本替换了

以下来自MDN:

Etags作为缓存的一种强校验器,ETag 响应头是一个对用户代理(User Agent, 下面简称UA)不透明(译者注:UA 无需理解,只需要按规定使用即可)的值。对于像浏览器这样的HTTP UA,不知道ETag代表什么,不能预测它的值是多少。如果资源请求的响应头里含有ETag, 客户端可以在后续的所有请求的头中带上 If-None-Match 头来验证缓存。
Last-Modified 响应头可以作为一种弱校验器。说它弱是因为它是一次性的。如果响应头里含有这个信息,客户端可以在后续的一次请求中带上 If-Modified-Since 来验证缓存。
当向服务端发起缓存校验的请求时,服务端会返回 200 ok表示返回正常的结果或者 304 Not Modified(不返回body)表示浏览器可以使用本地缓存文件。304的响应头也可以同时更新缓存文档的过期时间。

有关Vary

Vary用在响应首部

Vary: Accept-Language

假设源服务器在响应首部里带了上面这个Vary,意思告诉缓存代理服务器,我响应主体里的这份资源,你可以缓存下来(假设缓存下来的这份资源是en-US),当有客户端向你发送请求的时候,如果它的请求首部里面有Accept-Language: en-US你才能向他返回这个缓存,如果没有的话请再来向我发起一个新请求

参考资料

MDN HTTP 缓存
Web缓存机制系列 by腾讯AlloyTeam
彻底弄懂 HTTP 缓存机制 —— 基于缓存策略三要素分解法