背景&思考
在客户端请求资源时如果每次都向 server 发请求,server 把数据返回给客户端,不仅耗时还有大量的流量浪费。解法是——缓存,第一次请求时客户端将 response 保存起来,下次再请求时直接从缓存取数据,避免重复请求。其中还有很多细节:
- Q1:server的数据修改后,本地缓存的数据就过期了,每次都从本地取数据有数据不一致的风险。解法是给缓存的数据设置一个保质期,保质期内从缓存取数据,过保质期后请求 server。
- Q2:过保质期后请求 server,由于 server 的数据不一定会更新,还是有请求浪费的情况。
为解决上述问题,HTTP 协议在缓存策略上做了很多规定,本文章主要围绕上述两个问题讲解 HTTP 的规定。
1、Q1——强制缓存
客户端本地缓存有保质期,如何确定缓存数据是否有效呢?
1.1、Expires(HTTP/1.0)
HTTP 1.0 在 response 的 header 中加 Expires
:
Expires: Tue, 28 Feb 2022 22:22:22 GMT
Expires
:是资源将来的过期时间。下次发起请求时客户端会判断:
- 当前时间未超过
Expires
:缓存未过期,从缓存取数据,不发出请求。通过调试模式可以看到状态码后有(from memory
orfrom disk
) - 当前时间未超过
Expires
:表示缓存过期,会发出请求获取最新数据。
使用 Expires
的问题:
所以 HTTP 1.1 使用 Cache-Control
指定缓存策略。
1.2、Cache-Control:max-age(HTTP/1.1)
在 response 中设置 Cache-Control
,值有:
private:客户端可以缓存
public:客户端和代理服务器均可缓存;
max-age=xxx:缓存的资源将在 xxx 秒后过期;
no-cache:需要使用协商缓存来验证是否过期;
no-store:不可缓存
常用的是 max-age=xxx,意思是 xxx 秒后缓存过期。请求时缓存在有效期内则使用缓存:
超过有效期则会向 server 请求:
如果
Expires
和Cache-Control: max-age
都设置了,优先级是:max-age
>Expires
。由于 HTTP/1.1 已被广泛使用,无需特地提供Expires
。
1.3、测试 Demo
这里有个 Nodejs 写的 server Demo,可以使用 HTTPCacheServer 验证缓存策略。
图片 example.png 设置了 max-age=10,10秒内刷新可以看到使用的是缓存:
第二张图片
example1.png
是验证协商缓存。
2、Q2——协商缓存
协商缓存是为了处理强制缓存失效的情况。强制缓存失效后会去请求 server,需要一套规则来告诉客户端缓存是否真的过期。
2.1、Last-Modified/If-Modified-Since(HTTP/1.0)
Last-Modified
:资源上次修改的时间。server 会在每次请求的 response.header 会带上这个字段,下次客户端请求时将保存的 Last-Modified
作为 request.header 的 If-Modified-Since
,server 比较收到的 If-Modified-Since
和资源的最后修改的时间,判断客户端缓存是否有效:
If-Modified-Since
== 最后修改时间:缓存有效,返回 304 Not Modified,因此没有有响应主体。客户端收到该响应后,将存储的过期响应恢复为有效的,并可以在剩余的 max-aage 重复使用。If-Modified-Since
!= 最后修改时间:缓存无效,返回 202+最新数据。
Last-Modified/If-Modified-Since
的问题:
2.2、ETag/If-None-Match(HTTP/1.1)
HTTP 在 1.1 版本推出了 ETag
,解决了 Last-Modified/If-Modified-Since
的问题。
客户端第一次请求时,server 生成 ETag
,并作为 response.header 返回,客户端下次请求时把之前收到的 ETag
放到 request.header 的 If-Modified-Since
字段中,server 根据If-Modified-Since
和最新的 ETag
比较,如果匹配,则返回 304、没有有响应主体,否则返回 200、同时返回最新的资源。流程如下:
sequenceDiagram
客户端->>Server:第一次请求。
Server->>客户端:返回数据,ETag=XXY。
Note over 客户端:本地缓存过期
客户端->>Server:向 server 请求数据, If-None-Match=XXY
Server-->>客户端:case1:ETag相同:304。
Server-->>客户端:case2:ETag不同:200、最新资源。
ETag
、Last-Modified
同样有优先级:ETag
>Last-Modified
。
3、强制重新验证
某些情况下,如server资源经常变动,不让客户端使用缓存,希望始终从服务器获取最新内容,则可以使用 no-cache 指令强制验证。
通过在响应中添加 Cache-Control: no-cache 以及 Last-Modified 和 ETag,如下:
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
…
- 资源有更新:200 OK 响应
- 资源无更新:304 Not Modified 响应
Cache-Control: max-age=0, must-revalidate
的组合与no-cache
具有相同的含义。
- max-age=0 意味着响应立即过时,而 must-revalidate 意味着一旦过时就不得在没有重新验证的情况下重用它——因此,结合起来,语义似乎与 no-cache 相同。
- 然而,max-age=0 的使用是解决 HTTP/1.1 之前的许多实现无法处理 no-cache 这一指令——因此为了解决这个限制,max-age=0 被用作解决方法。
4、不使用缓存
no-cache
指令不会阻止响应的存储,而是阻止在没有重新验证的情况下使用本地缓存。如果不希望将数据存储在任何缓存中,请使用 no-store
。
Cache-Control: no-store
5、Reference
- mdn-html
- 阿里一面:HTTP 1.0 和 HTTP 1.1 有什么区别?
- 一文讲透HTTP缓存之ETag
- Expires、Cache-Control、Last-Modified和If-Modified—Since、Etag和If-None-Match
- HTTP1.0、1.1、2.0协议的特性及区别
- 10分钟彻底搞懂Http的强制缓存和协商缓存
- 手把手教你如何实现服务器强缓存&协商缓存(nodejs实战篇)
- nodejs 中的 Cache-Control