缓存是一个老生常谈的问题,重要性不言而喻,HTTP 协议中规定了很多请求头和响应头来控制缓存。也因为如此,很多人无法分清某个头部的作用和优先级。本文尝试做一下梳理和总结。

经典 GET 请求过程

先看一个经典的 GET 请求的处理过程,如下图: IMAGE 当一个请求达到时,浏览器(为方便叙述,已浏览器为例)先检查被访问的资源是否已被缓存,如果未被缓存(缓存未命中 cache miss),则将请求转发给原始服务器。如果被缓存(缓存命中,cache hit),则会检查缓存是否足够新鲜。如果缓存的副本足够新鲜,则直接将副本返回给客户端,否则会向服务端发起新鲜度验证(revalidation)。如果发现与服务端文件一致,则将本地缓存副本返回给客户端,否则将请求转发给原始服务器。

在这个过程中,由缓存提供服务的请求所在的比例称为缓存命中率(cache hit rat)。这种描述方式只能描述请求级别的命中情况,无法体现具体有多少流量来自缓存。比如一个访问频次很低,尺寸很大的文件,如果以该命中率来描述的话,命中率非常低。但是这个文件却占据了绝大多数的访问流量。因此还需要另一个命中率指标来描述,那就是字节命中率(byte hit rate)。字节命中率表示的是缓存提供的字节在传输的所有字节中所占的比例。下面章节中服务器再验证的两种策略即是这两种命中率的具体使用。

缓存机制

上图中,HTTP 通过一些简单的机制在不要求服务器记住有哪些缓存拥有其文档副本的情况下,保持已缓存数据与服务器数据之间充分一致。这些机制可以分为两个部分,第一部分称为文档过期(document expiration),第二部分称为服务端再验证(server revalidation。

文档过期

通过 Cache-Control:max-age 首部和 Expires 首部,HTTP 让原始服务器向每个文档附加一个“过期日期”。在缓存文档过期之前,缓存可以以任意频次使用这些副本,而无需与服务端联系。 Expires 首部与 Cache-Control:max-age 首部本质上是一样的,区别是 Expires 是 HTTP/1.0 协议规定的首部,且首部取值为一个绝对时间,在这个时间之后缓存失效;Cache-Control:max-age 是 HTTP/1.1 协议规定的首部,且首部取值是一个相对时间,单位为秒。

服务器再验证

HTTP 定义了 5 个条件请求首部来完成服务器再验证。

  • If-Modified-Since
  • If-None-Match
  • If-Unmodified-Since
  • If-Range
  • If-Match

其中最有用的是 If-Modified-SinceIf-None-Match 两个首部。

If-Modified-Since: Date 再验证

If-Modified-Since: Date 再验证请求工作方式如下:

  • 如果自指定日期后,文档被修改了,If-Modified-Since 条件为真,GET 请求就会执行。携带新首部的新文档会被返回给缓存,新首部除了其他信息以外,还包含了一个新的过期日期。
  • 入股自指定日期后,文档没有被修改过,条件就为假,会向客户端返回一个小的 304 Not Modified 响应报文,为了提高有效性,一般会发送一个新的过期日期,不会返回文档的主体。

If-Modified-Since 请求首部通常与 Last-Modified 服务器响应首部配合工作。原始服务器会将最后的修改日期附加到文档上去。当缓存要对已缓存的文档进行再验证时,就会包含一个 If-Modified-SinceIf-Modified-Since 首部,其中携带有最后修改已缓存副本额日期:

If-Modified-Since: <cached last-modified date>
1

If-None-Match: etag 实体标签验证

有些情况下,If-Modified-Since: Date 再验证无法很好的解决缓存问题。比如一个被周期性复写的文件,但是文件的内容往往是一样的。这种情况下,就需要借助实体标签(Etag)验证了。实体标签就是“版本标识符”,是附加到文档上的任意标签(引用字符串),可能包含了文档的序列号或版本名,或者是文档内容的校验信息。 If-None-Match: etag 实体标签验证的工作过程与 If-Modified-Since: Date 再验证的工作过程基本一致,不同的是,服务器会在响应中附加一个 Etag 响应头。当缓存要对已缓存的文档进行再验证时,就会将这个 etag 放到 If-None-Match 请求头中去。

服务器控制缓存的能力

服务器也可以通过如下方式控制缓存,优先级一次递减:

  • Cache-Control: no-store 禁止缓存对响应进行复制。
  • Cache-Control: no-cache/ Pragma: no-cache 缓存可以复制响应,但是在与原始服务器进行新鲜度再验证之前不能将其提供给客户端。Pramga: no-cache 为了兼容 HTTP/1.0,优先级低于 Cache-Control: no-cache。
  • Cache-Control: must-revalidate 在事前没有跟原始服务器进行再验证的情况下,缓存不能提供缓存副本。
  • Cache-Control: max-age max-age 指定的秒数内有效。max-age 为零时,不可缓存。
  • Expires: Date 在实际的绝对日期之前有效。

客户端的新鲜度控制

客户端通过 Cache-Control 请求首部来强化或放松对过期时间的限制。

  • Cache-Control: max-stale=< s > 缓存可以随意提供副本,如果指定的秒数,那么在这段时间内,文档不能过期。
  • Cache-Control: min-fresh=< s > 至少在未来< s >秒内文档保持新鲜。
  • Cache-Control: max-age=< s > 缓存无法返回缓存时间超过< s >的文档。如果与 max-stale 通用,max-stale 优先级更高。
  • Cache-Control: no-cache/Pragma: no-cache 除非进行了再验证,否则客户端不接受已缓存的资源。
  • Cache-Control: no-store 缓存应该删除本地缓存副本,使用原始服务器响应。
  • Cache-Control: only-if-cache 只有当缓存中有副本存在时,客户端才会获取一份副本。

小结

合理的缓存策略可以帮助我们减少冗余数据传输,节省带宽,同时加快响应速度。不当的缓存策略也可能导致客户端一直使用过期的缓存副本,无法得到及时更新。因此,在搞清楚缓存机制后,根据业务需要进行合理配置才是有效使用缓存的正确姿势。

关注微信公众号,获取最新推送~

加微信,深入交流~