4.1.2 协商缓存

强制缓存是基于时效性的,但无论是人还是服务器,其实多数情况下并没有什么把握去承诺某项资源多久不会发生变化。另外一种基于变化检测的缓存机制,在一致性上会有比强制缓存更好的表现,但需要一次变化检测的交互开销,性能上就会略差一些,这种基于检测的缓存机制,通常被称为“协商缓存”。另外,应注意在HTTP中的协商缓存与强制缓存并没有互斥性,这两套机制是并行工作的。譬如,当强制缓存存在时,直接从强制缓存中返回资源,无须进行变动检查;而当强制缓存超过时效,或者被禁止(no-cache/must-revalidate)时,协商缓存仍可以正常工作。协商缓存有两种变动检查机制,分别是根据资源的修改时间进行检查,以及根据资源唯一标识是否发生变化进行检查,它们都是靠一组成对出现的请求、响应Header来实现的。

1.Last-Modified和If-Modified-Since

Last-Modified是服务端的响应Header,用于告诉客户端这个资源的最后修改时间。对于带有这个Header的资源,当客户端需要再次请求时,会通过If-Modified-Since把之前收到的资源最后修改时间发送回服务端。

如果此时服务端发现资源在该时间后没有被修改过,就返回一个304/Not Modified的响应,无须附带消息体,即可达到节省流量的目的,如下所示:


HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=600
Last-Modified: Wed, 8 Apr 2020 15:31:30 GMT

如果此时服务端发现资源在该时间之后有变动,就会返回200/OK的完整响应,在消息体中包含最新的资源,如下所示:


HTTP/1.1 200 OK
Cache-Control: public, max-age=600
Last-Modified: Wed, 8 Apr 2020 15:31:30 GMT

Content

2.ETag和If-None-Match

ETag是服务端的响应Header,用于告诉客户端这个资源的唯一标识。HTTP服务端可以根据自己的意愿来选择如何生成这个标识,譬如Apache服务端的ETag值默认是对文件的索引节点(INode)、大小和最后修改时间进行哈希计算后得到的。对于带有这个Header的资源,当客户端需要再次请求时,会通过If-None-Match把之前收到的资源唯一标识发送回服务端。

如果此时服务端计算后发现资源的唯一标识与上传回来的标识一致,说明资源没有被修改过,就返回一个304/Not Modified的响应,无须附带消息体,即可达到节省流量的目的,如下所示:


HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=600
ETag: "28c3f612-ceb0-4ddc-ae35-791ca840c5fa"

如果此时服务端发现资源的唯一标识有变动,就会返回200/OK的完整响应,在消息体中包含最新的资源,如下所示:


HTTP/1.1 200 OK
Cache-Control: public, max-age=600
ETag: "28c3f612-ceb0-4ddc-ae35-791ca840c5fa"

Content

ETag是HTTP中一致性最强的缓存机制,譬如,Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1s以内被修改多次的话,它将不能准确标注文件的修改时间;又如果某些文件会被定期生成,可能内容并没有任何变化,但Last-Modified却改变了,导致文件无法有效使用缓存,这些情况Last-Modified都有可能产生资源一致性问题,只能使用ETag解决。

ETag也是HTTP中性能最差的缓存机制,在每次请求时,服务端都必须对资源进行哈希计算,相比简单获取一下修改时间,开销要大了很多。ETag和Last-Modified是允许一起使用的,服务端会优先验证ETag,在ETag一致的情况下,再去对比Last-Modified,这是为了防止有一些HTTP服务端未将文件修改日期纳入哈希范围内。

到这里为止,HTTP的协商缓存机制已经能很好地适用于通过URL获取单个资源的场景,为什么要强调“单个资源”呢?在HTTP协议的设计中,一个URL地址是有可能提供多份不同版本的资源的,譬如,一段文字的不同语言版本,一个文件的不同编码格式版本,一份数据的不同压缩方式版本,等等。因此针对请求的缓存机制,也必须能够提供对应的支持。为此,HTTP协议设计了以Accept*(Accept、Accept-Language、Accept-Charset、Accept-Encoding)开头的一套请求Header和对应的以Content-*(Content-Language、Content-Type、Content-Encoding)开头的响应Header,这些Header被称为HTTP的内容协商机制。与之对应的,对于一个URL能够获取多个资源的场景,缓存也同样需要有明确的标识来获知根据什么内容返回给用户正确的资源。此时就要用到Vary Header,Vary后面应该跟随一组其他Header的名字,譬如:


HTTP/1.1 200 OK
Vary: Accept, User-Agent

以上响应的含义是应该根据MIME类型和浏览器类型来缓存资源,获取资源时也需要根据请求Header中对应的字段来筛选出适合的资源版本。

根据约定,协商缓存不仅在浏览器的地址输入、页面链接跳转、新开窗口、前进、后退中生效,而且在用户主动刷新页面(F5)时同样是生效的,只有用户强制刷新(Ctrl+F5)或者明确禁用缓存(譬如在DevTools中设定)时才会失效,此时客户端向服务端发出的请求会自动带有“Cache-Control:no-cache”。