HTTP 缓存不完全指南

Cache is a hardware or software component that stores data so that future requests for that data can be served faster.

按照维基百科的定义,缓存是一种用来存储数据的硬件或者软件,它使得后续的信息传输更快。

从一张图片的加载说起

这是这一张图片的响应头头部,其中包含 last-modified 字段。

1
2
3
4
HTTP/1.1 200 OK
content-length: 39409
last-modified: Thu, 14 May 2020 14:29:20 GMT
x-response-time: 7ms

那么就让我们从这两个问题开始:

  • 当我们刷新浏览器的时候,图片如何加载?是否会使用缓存?
  • 如果我们添加一个 cache-control: no-cache 的响应头部,再次刷新图片又该如何加载?

为什么我们要使用缓存

  • 减少冗余数据传输
  • 节省带宽
  • 避免瞬间拥塞
    如明星出轨的新闻会使微博奔溃一样,其背后的原因就是瞬间有大量的用户共同访问同一个资源,这就使得网络瞬间拥塞(Flash Crowds)。使用缓存可以分散用户流量,避免瞬间拥塞。
  • 距离时延
    信息以光速在互联网中传输,但是如果客户端访问的资源较远,客户端的请求仍然需要耗费几百毫秒甚至几秒的时间才能抵达服务器,服务器的响应也需要经过近似长的时间才能抵达客户端。如果能命中客户端的缓存或者离客户端较近的缓存,那么就可以缩短距离时延。

HTTP 缓存的三种状态

一般而言,缓存只有两种状态,即存活和过期,一旦缓存过期,会有定时器将其清除。和其他缓存软件不一样的是,HTTP 缓存中定义了三种缓存状态:

  1. 缓存命中
  2. 缓存未命中
  3. 缓存再验证中

缓存的第一二种状态和常规缓存软件一样,不同的是第三种状态 - 缓存再验证。和其他缓存软件不一样的是,HTTP 缓存过期后并不会立即被删除,所以当新的请求命中了过期后的缓存时,会携带 if-none-matchlast-modified 头部请求服务器对资源做再验证。

再验证的目的是判断过期后的缓存是否可用。如果在这段时间内资源并未发生变化,那么即使资源是过期的,但依然与最新的资源内容一致,然后服务端会使用 304 状态码告诉客户端可使用缓存,再验证通过。如果资源发生了改变,那么服务端会返回最新的资源,并覆盖掉缓存,再验证失败。

HTTP 缓存的处理流程

如之前所述,HTTP 缓存的处理流程如下:

cache flow

HTTP 缓存机制

mechanism

HTTP 缓存具有三个状态,与之对应也有三个策略,即存储策略、过期策略和再验证策略。 客户端和服务端使用响应的头部控制这些策略。

存储策略

服务端下发的响应头部中包含了一个请求头 Cache-Control,其部分取值即用于控制客户端的缓存行为(并非客只有客户端实现了HTTP 缓存协议,如 CDN 一样的中间代理也会实现 HTTP 缓存)。Cache-Control 用于控制存储策略的部分取值如下:

  • private - 私有缓存,数据只能存储在客户端,缓存不可共享。
  • public - 共有缓存,数据可以存储在任何地方,如 CDN 等,缓存可共享。
  • no-store - 数据不能被存储,无论是私有还是共有,如果有缓存,缓存客户端应该尽快删除。

过期策略

Cache-Control可以有多个取值,多个值之间使用都好分割。除了控制存储策略的取值外,还包含控制过期策略的取值:

  • max-age=x - 设置缓存的生存时间是 x 秒,x 秒能命中缓存可以直接使用, x 秒后如果仍然命中缓存那么会对缓存做再验证,如果验证通过则使用。
  • no-cache - 相当于 max-age=0其含义是说数据可以被存储,但是再验证之前不能直接使用

除了 Cache-Control 外,早期 HTTP/1.0 版本是使用 expires 头部控制过期策略的,max-age 是 HTTP/1.1 版本引入的,如果 Cache-Control: max-age=xExpires: xx 都设置了,会优先使用 max-age 控制过期策略。

再验证策略

再验证策略需要客户端和服务端的共同配合,因此共有四个相关的首部,总结如下

名字 版本 作用头部 含义
Etag HTTP/1.1 响应头 响应体的唯一性哈希值
Last-Modified HTTP/1.0 响应头 响应体(资源)最后修改时间
If-None-Match HTTP/1.1 请求头 使用该请求头携带上次服务端下方的 etag
If-Modified-Since HTTP/1.0 请求头 该请求发出的时间,用于判断资源在该时间之前是否改变

此前,我们所探讨的 Cache-Control 是作用在响应头部的,值得注意的是,该头部也可以作用在请求头部。Cache-Control 作用在请求头部最常用的取值是 no-cache ,表示当前请求不能使用任何缓存,一般用于支持端到端的强制刷新。 当我们禁用缓存刷新页面时,浏览器就会携带该请求头。

试探性过期策略

如果存储策略、过期策略和再验证策略都缺失,那么就无法使用任何 HTTP 缓存,也就是说系统没有实现 HTTP 缓存协议。

一般而言,服务器都会为静态资源生成 Last-Modified 头部,即再验证策略可用。这种情况下如果服务端没有指定存储策略也没有指定过期策略,那么缓存客户端会使用试探性过期策略算法推算出资源过期时间。试探性过期策略算法如下:

1
2
const TimeSinceModify = max(0, Current - LastModified);
const MaxAge = int(TimeSinceModify * LMFactor);

其中 LMFactor 取值如下:

现在, 让我们回顾文章开头的两个问题

  • 当我们刷新浏览器的时候,图片如何加载?是否会使用缓存?
    我们知道,响应头中只包括再验证策略 - Last-Modified 头部,因此客户端会缓存图片并且使用试探性过期策略推导出缓存时间。因此,再次刷新图片时理论上会命中客户端缓存。
  • 如果我们在响应头部中添加一个 cache-control: no-cache 的响应头部,那多次刷新图片又该如何加载?
    设置 no-cache 后,即添加了过期策略, no-cache 表示不会再验证之前不会使用缓存,因此理论上再次刷新服务端会返回 304 并从客户端缓存中读取图片。

参考资料

Useful links && reference

本文标题:HTTP 缓存不完全指南

文章作者:Pylon, Syncher

发布时间:2020年05月18日 - 21:05

最后更新:2023年03月11日 - 17:03

原始链接:https://0x400.com/fundamental/network/http/dev-http-caching/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。