HTTP——缓存

431 阅读5分钟

背景&思考

在客户端请求资源时如果每次都向 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 or from disk
  • 当前时间超过Expires:表示缓存过期,会发出请求获取最新数据。

使用 Expires 的问题:

  1. 时间格式难以解析,会引发一些问题。
  2. 如果手动调整设备的时间,缓存策略就不准了。

所以 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 秒后缓存过期。请求时缓存在有效期内则使用缓存:

image.png

超过有效期则会向 server 请求:

image.png

如果 ExpiresCache-Control: max-age 都设置了,优先级是:max-age > Expires。由于 HTTP/1.1 已被广泛使用,无需特地提供 Expires

1.3、测试 Demo

这里有个 Nodejs 写的 server Demo,可以使用 HTTPCacheServer 验证缓存策略。

HTTPCacheServrDemo.png

图片 example.png 设置了 max-age=10,10秒内刷新可以看到使用的是缓存:

image.png

第二张图片 example1.png 是验证协商缓存。

2、Q2——协商缓存

当通过 ExpiresCache-Control 判断本地缓存数据过期后,server 的数据和本地缓存的数据一定有差异吗?这时需要发请求向 server 询问了,HTTP 定义了一套规则来告诉客户端缓存是否真的过期。

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+最新数据。

image.pngimage.png Last-Modified/If-Modified-Since 的问题

  1. 时间的最小单位是秒,如果在一秒内有更改将不能被识别。
  2. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新发请求。
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、最新资源。

ETagLast-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

<!doctype html>
  • 资源有更新: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