1、缓存类型
1.1、私有缓存
私有缓存是只存在于客户端的缓存(通常是浏览器缓存),禁止代理服务器、CDN缓存。
Cache-Control: private
1.2、共享缓存
-
代理缓存
代理缓存是由网络中的代理服务器实现的,这些代理服务器位于客户端和原始服务器之间;一些代理还实现了缓存以减少网络流量。这通常不由服务开发人员管理,因此必须由恰当的 HTTP 标头等控制。然而,近年来,随着 HTTPS 变得越来越普遍,客户端/服务器通信变得加密,在许多情况下,路径中的代理缓存只能传输响应而不能充当缓存。因此,在这种情况下,无需担心甚至无法看到响应的过时代理缓存的实现。
-
托管缓存
托管缓存由服务开发人员明确部署,以降低源服务器负载并有效地交付内容。托管缓存的特性因部署的产品而异。在大多数情况下,你可以通过
Cache-Control
标头和你自己的配置文件或仪表板来控制缓存的行为。
2、缓存方式
2.1、启发式缓存 (过时)
HTTP 旨在尽可能多地缓存,因此即使没有给出 Cache-Control,如果满足某些条件,响应也会被存储和重用。这称为启发式缓存。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2021 22:22:22 GMT
<!doctype html>
整整一年没有更新的内容在那之后的一段时间内不会更新。因此,客户端存储此响应(尽管缺少 max-age
)并重用它一段时间。复用多长时间取决于实现,但规范建议存储后大约 10%(在本例中为 0.1 年)的时间。
启发式缓存是在 Cache-Control
被广泛采用之前出现的一种解决方法,基本上所有响应都应明确指定 Cache-Control 标头。
2.2、age
一般应用在代理服务器中。
如果有Age和max-age,则缓存时间为max-age减去Age
存储的 HTTP 响应有两种状态:fresh 和 stale。fresh 状态通常表示响应仍然有效,可以重复使用,而 stale 状态表示缓存的响应已经过期。
确定响应何时是 fresh 的和何时是 stale 的标准是 age
。在 HTTP 中,age 是自响应生成以来经过的时间。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
<!doctype html>
存储示例响应的缓存会计算响应生成后经过的时间,并将结果用作响应的 age。
对于该示例的响应,max-age
的含义如下:
如果响应的 age 小于一周,则响应为 fresh。
如果响应的 age 超过一周,则响应为 stale。
只要存储的响应保持有效(fresh),它将用于兑现客户端请求。
当响应存储在共享缓存中时,有必要通知客户端响应的 age。继续看示例,如果共享缓存将响应存储了一天,则共享缓存将向后续客户端请求发送以下响应。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
Age: 86400
<!doctype html>
…
收到该响应的客户端会发现它在剩余的 518400 秒内是有效的,这是响应的 max-age
和 Age
之间的差异。
2.3、Expires(已过时) 或 max-age
在 HTTP/1.0 中,有效期是通过 Expires
标头来指定的。
Expires
标头使用明确的时间而不是通过指定经过的时间来指定缓存的生命周期。
但是时间格式难以解析,也发现了很多实现的错误,有可能通过故意偏移系统时钟来诱发问题;因此,在 HTTP/1.1 中,Cache-Control
采用了 max-age
——用于指定经过的时间。
如果 Expires 和 Cache-Control: max-age
都可用,则将 max-age
定义为首选。因此,由于 HTTP/1.1 已被广泛使用,无需特地提供 Expires。
2.4、Vary响应头
响应的内容并不总是相同的,即使它们具有相同的 URL。特别是在执行内容协商时,来自服务器的响应可能取决于 Accept
、Accept-Language
和 Accept-Encoding
请求标头的值。
例如,对于带有 Accept-Language: en 标头并已缓存的英语内容,不希望再对具有 Accept-Language: ja 请求标头的请求重用该缓存响应。在这种情况下,你可以通过在 Vary
标头的值中添加“Accept-Language”,根据语言单独缓存响应。
Vary: Accept-Language
这会导致缓存基于响应 URL 和 Accept-Language请求标头的组合进行键控——而不是仅仅基于响应 URL。
此外,如果你基于用户代理提供内容优化(例如,响应式设计),你可能会想在 Vary 标头的值中包含“User-Agent”。但是,User-Agent 请求标头通常具有非常多的变体,这大大降低了缓存被重用的机会。因此,如果可能,请考虑一种基于特征检测而不是基于 User-Agent 请求标头来改变行为的方法。
对于使用 cookie 来防止其他人重复使用缓存的个性化内容的应用程序,你应该指定 Cache-Control: private 而不是为 Vary 指定 cookie。
2.5、验证响应(If-Modified-Since、If-None-Match)
过时的响应不会立即被丢弃。HTTP 有一种机制,可以通过询问源服务器将陈旧的响应转换为新的响应。这称为验证,有时也称为重新验证。
验证是通过使用包含 If-Modified-Since 或 If-None-Match 请求标头的条件请求完成的。
2.5.1、If-Modified-Since
以下响应在 22:22:22 生成,max-age 为 1 小时,因此你知道它在 23:22:22 之前是有效的。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600
<!doctype html>
…
到 23:22:22 时,响应会过时并且不能重用缓存。因此,下面的请求显示客户端发送带有 If-Modified-Since 请求标头的请求,以询问服务器自指定时间以来是否有任何的改变。
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT
如果内容自指定时间以来没有更改,服务器将响应 304 Not Modified。
由于此响应仅表示“没有变化”,因此没有响应主体——只有一个状态码——因此传输大小非常小。
HTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600
收到该响应后,客户端将存储的过期响应恢复为有效的,并可以在剩余的 1 小时内重复使用它。
服务器可以从操作系统的文件系统中获取修改时间,这对于提供静态文件的情况来说是比较容易做到的。但是,也存在一些问题;例如,时间格式复杂且难以解析,分布式服务器难以同步文件更新时间。
为了解决这些问题,ETag 响应标头被标准化作为替代方案。
2.5.2、ETag/If-None-Match
ETag 响应标头的值是服务器生成的任意值。服务器对于生成值没有任何限制,因此服务器可以根据他们选择的任何方式自由设置值——例如主体内容的哈希或版本号。
举个例子,如果 ETag 标头使用了 hash 值,index.html 资源的 hash 值是 deadbeef,响应如下:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "deadbeef"
Cache-Control: max-age=3600
<!doctype html>
…
如果该响应是陈旧的,则客户端获取缓存响应的 ETag 响应标头的值,并将其放入 If-None-Match 请求标头中,以询问服务器资源是否已被修改:
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-None-Match: "deadbeef"
如果服务器为请求的资源确定的 ETag
标头的值与请求中的If-None-Match
值相同,则服务器将返回 304 Not Modified。
但是,如果服务器确定请求的资源现在应该具有不同的 ETag 值,则服务器将其改为 200 OK 和资源的最新版本进行响应。
备注: 在评估如何使用 ETag 和 Last-Modified 时,请考虑以下几点:在缓存重新验证期间,如果 ETag 和 Last-Modified 都存在,则 ETag 优先。因此,如果你只考虑缓存,你可能会认为 Last-Modified是不必要的。然而,Last-Modified 不仅仅对缓存有用;相反,它是一个标准的 HTTP 标头,内容管理 (CMS)系统也使用它来显示上次修改时间,由爬虫调整爬取频率,以及用于其他各种目的。所以考虑到整个 HTTP 生态系统,最好同时提供 ETag 和 Last-Modified。
2.5.3、强制重新验证
如果你不希望重复使用响应,而是希望始终从服务器获取最新内容,则可以使用 no-cache 指令强制验证。
通过在响应中添加 Cache-Control: no-cache 以及 Last-Modified 和 ETag——如下所示——如果请求的资源已更新,客户端将收到 200 OK 响应,否则,如果请求的资源尚未更新,则会收到 304 Not Modified 响应。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: deadbeef
Cache-Control: no-cache
<!doctype html>
…
max-age=0 和 must-revalidate 的组合与 no-cache 具有相同的含义。
Cache-Control: max-age=0, must-revalidate
max-age=0 意味着响应立即过时,而 must-revalidate 意味着一旦过时就不得在没有重新验证的情况下重用它——因此,结合起来,语义似乎与 no-cache 相同。
然而,max-age=0 的使用是解决 HTTP/1.1 之前的许多实现无法处理 no-cache
这一指令——因此为了解决这个限制,max-age=0 被用作解决方法。
但是现在符合 HTTP/1.1 的服务器已经广泛部署,没有理由使用 max-age=0 和 must-revalidate
组合——你应该只使用 no-cache
。
注意:no-cache 的意思不是禁止缓存,而是强制使用协商缓存
2.6、不使用缓存
Cache-Control: no-store
2.6.1、no-store 丢失了什么
你可能认为添加 no-store
是选择退出缓存的正确方法。
但是,不建议随意授予 no-store
,因为你失去了 HTTP 和浏览器所拥有的许多优势,包括浏览器的后退/前进缓存。
因此,要获得 Web 平台的全部功能集的优势,最好将 no-cache
与 private
结合使用。
3、重新加载和强制重新加载(浏览器行为)
3.1、重新加载
为了从页面错误中恢复或更新到最新版本的资源,浏览器为用户提供了重新加载功能。
在浏览器重新加载期间发送的 HTTP 请求的简化视图如下所示:
GET / HTTP/1.1
Host: example.com
Cache-Control: max-age=0
If-None-Match: "deadbeef"
If-Modified-Since: Tue, 22 Feb 2022 20:20:20 GMT
请求中的 max-age=0
指令指定“重用 age 为 0 或更少的响应”——因此,中间存储的响应不会被重用。
请求通过 If-None-Match
和 If-Modified-Since
进行验证。
3.2、强制重新加载
出于向后兼容的原因,浏览器在重新加载期间使用 max-age=0
——因为在 HTTP/1.1
之前的许多过时的实现中不理解 no-cache
。但是在这个用例中,no-cache
已被支持,并且强制重新加载是绕过缓存响应的另一种方法。
浏览器强制重新加载期间的 HTTP 请求如下所示:
GET / HTTP/1.1
Host: example.com
Pragma: no-cache
Cache-Control: no-cache
由于这不是带有 no-cache 的条件请求,因此你可以确定你会从源服务器获得 200 OK。
3.3、immutable 避免重新验证
永远不会改变的内容应该被赋予一个较长的 max-age
,方法是使用缓存破坏——也就是说,在请求 URL 中包含版本号、哈希值等。
但是,当用户重新加载时,即使服务器知道内容是不可变的,也会发送重新验证请求。
为了防止这种情况,immutable
指令可用于明确指示不需要重新验证,因为内容永远不会改变。
Cache-Control: max-age=31536000, immutable
这可以防止在重新加载期间进行不必要的重新验证。
当前兼容性不好
4、删除存储的响应
基本上没有办法删除用很长的 max-age
存储的响应。
5、请求折叠
共享缓存主要位于源服务器之前,旨在减少到源服务器的流量。
因此,如果多个相同的请求同时到达共享缓存,中间缓存将代表自己将单个请求转发到源,然后源可以将结果重用于所有客户端。这称为请求折叠。
当请求同时到达时会发生请求折叠,因此即使响应中给出了 max-age=0
或 no-cache
,它也会被重用。
如果响应是针对特定用户个性化的,并且你不希望它在折叠中共享,则应添加 private
指令
6、常见的缓存模式
6.1、默认设置
如上所述,缓存的默认行为(即对于没有 Cache-Control
的响应)不是简单的“不缓存”,而是根据所谓的“启发式缓存”进行隐式缓存。
为确保默认情况下始终传输最新版本的资源,通常的做法是让默认的 Cache-Control
值包含 no-cache
:
Cache-Control: no-cache
另外,如果服务实现了 cookie 或其他登录方式,并且内容是为每个用户个性化的,那么也必须提供 private,以防止与其他用户共享:
Cache-Control: no-cache, private
6.2、缓存破坏
<script src="bundle.js"></script>
<link rel="stylesheet" href="build.css" />
<body>
hello
</body>
在现代 Web 开发中,JavaScript 和 CSS 资源会随着开发的进展而频繁更新。此外,如果客户端使用的 JavaScript 和 CSS 资源的版本不同步,则显示将中断。
所以上面的 HTML 用 max-age 缓存 bundle.js 和 build.css 变得很困难。
因此,你可以使用包含基于版本号或哈希值的更改部分的 URL 来提供 JavaScript 和 CSS。一些方法如下所示。
# version in filename
bundle.v123.js
# version in query
bundle.js?v=123
# hash in filename
bundle.YsAIAAAA-QG4G6kCMAMBAAAAAAAoK.js
# hash in query
bundle.js?v=YsAIAAAA-QG4G6kCMAMBAAAAAAAoK
通过这种设计,JavaScript 和 CSS 资源都可以被缓存很长时间。那么 max-age 应该设置多长时间呢?QPACK 规范提供了该问题的答案。
一些常用的缓存头值如下所示。
36 cache-control max-age=0
37 cache-control max-age=604800
38 cache-control max-age=2592000
39 cache-control no-cache
40 cache-control no-store
41 cache-control public, max-age=31536000
如果你选择其中一个编号选项,则可以在通过 HTTP3 传输时将值压缩为 1 个字节。
6.3、验证响应
不要忘记设置 Last-Modified 和 ETag 标头,以便在重新加载时不必重新传输资源。对于预构建的静态文件生成这些标头很容易。
可以添加 immutable 以防止重新加载时验证。
# bundle.v123.js
200 OK HTTP/1.1
Content-Type: application/javascript
Content-Length: 1024
Cache-Control: public, max-age=31536000, immutable
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK
6.4 主要资源
与子资源不同,主资源不能使用缓存破坏,因为它们的 URL 不能像子资源 URL 一样被修饰。
如果存储以下 HTML 本身,即使在服务器端更新内容,也无法显示最新版本。
<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>
对于这种情况,no-cache 将是合适的——而不是 no-store——因为我们不想存储 HTML,而只是希望它始终是最新的。
此外,添加 Last-Modified
和 ETag
将允许客户端发送条件请求,如果 HTML 没有更新,则可以返回 304 Not Modified:
200 OK HTTP/1.1
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE
注意:如果
ETag
采用弱验证器,则可能存在文件内容变化而ETag未变化的情况! 参考文档
7、总结
7.1、名词解释
7.1.1、强制缓存
客户端根据响应头(一般是Cache-Control
)分析当前缓存是否有效,如果有效,则直接使用缓存的副本,不需要与服务器进行通信。
7.1.2、协商缓存
客户端缓存失效后,向服务端发起请求,确定缓存是否有效(一般是If-Modified-Since
和 If-None-Match
)。如果资源未更改,则返回304(Not Modified),客户端可以继续使用缓存,否则服务器将返回新的资源内容。
7.2、缓存对比说明
缓存方式 | 类别 | 说明 | 使用方式 | 添加者 |
---|---|---|---|---|
启发式缓存 | 强制缓存 | HTTP 旨在尽可能多地缓存,因此即使没有给出 Cache-Control,如果满足某些条件,响应也会被存储和重用 | 只给出 Last-Modified | 服务器 |
age | 自响应生成以来经过的时间; 由代理服务器添加 | 与max-age搭配使用 | ||
expires | 强制缓存 | 设置过期时间 | Expires: Wed, 21 Oct 2015 07:28:00 GMT | 服务器 |
max-age | 强制缓存 | 从第一次请求资源的时候开始,往后N秒内,若再次请求,则直接从内存中读取;若有Age和max-age,缓存时间为max-age减去Age | 可以与协商缓存搭配使用 | 服务器;浏览器 |
Vary响应头 | 强制缓存 | Vary响应头的主要目的是帮助缓存系统判断何时可以安全地使用缓存的响应来满足新的请求;根据指定的请求头分类缓存 | 例:Vary: User-Agent | 服务器 |
no-cache | 强制缓存 | 强制进行协商缓存 | 与 Last-Modified 和 ETag搭配使用 | 服务器;浏览器 |
no-store | 强制缓存 | 不缓存 | Cache-Control: no-store | 服务器;浏览器 |
immutable | 强制缓存 | immutable 指令可用于明确指示不需要重新验证,因为内容永远不会改变。当前兼容性不好 | 当用户重新加载时,明确指示不需要重新验证 | 服务器 |
If-Modified-Since | 协商缓存 | 从Last-Modified到请求发出是否有变更 | 与Last-Modified搭配 | If-Modified-Since:浏览器 |
ETag/If-None-Match | 协商缓存 | 通过对比根据文档内容生成的唯一值是否变更来决定是否命中缓存 | 与max-age搭配使用 | ETag:服务器;If-None-Match:浏览器 |