0. 前言:

在web中,http请求一般都是浏览器发起的,所以我们这里所说的http的缓存策略,其实也就是浏览器端的缓存策略,因为http本身只是一种协议,真正实现缓存还是要靠浏览器(其实就是浏览器指定存储在硬盘下。)

我们使用 HTTP 缓存,通过复用缓存资源,减少了客户端等待时间和网络流量,同时也能缓解服务器端的压力。可以显著的提升我们网站和应用的性能。虽然 HTTP 缓存不是必须的,但重用缓存的资源通常是必要的,HTTP 缓存是一个 web 性能优化的重要手段。

要想实现一个完整的缓存,需要考虑很多因素。例如:

  1. 请求的资源发生改变的时候,如何让浏览器去获取新的资源。
  2. 设置缓存失效时间之后,如果服务器资源没有发生改变,浏览器如何判断。

什么样的HTTP响应会被客户端缓存?

  • 默认情况下,请求方法如 GET、HEAD的响应内容是可缓存的,在包含新鲜度信息的情况下,POST的响应内容也可以被缓存;

  • 默认情况下,响应码如 200、206、300、301、302、404 等的响应内容可以被缓存;

  • 响应头和请求头没有指明不使用缓存,如 Cache-Control: no-store。 以上是几种比较常见的情况。

MDN:

  • HTTP 缓存会存储与请求关联的响应,并将存储的响应复用于后续请求。
  • 首先,由于不需要将请求传递到源服务器,因此客户端和缓存越近,响应速度就越快。最典型的例子是浏览器本身为浏览器请求存储缓存。
  • 当响应可复用时,源服务器不需要处理请求——因为它不需要解析和路由请求、根据 cookie 恢复会话、查询数据库以获取结果或渲染模板引擎。这减少了服务器上的负载。

1. HTTP缓存类型

通常 HTTP 缓存类型分为两种:私有缓存:浏览器(强缓存)和共享缓存:各种中间代理服务器(协商缓存)。从字面意思我们可以很直观的看到它们的差别。强缓存即强制直接使用缓存。协商缓存就得和服务器协商确认下这个缓存能不能用。

私用缓存:

  • 仅供一个客户端使用的缓存,即客户端上的缓存仅供自己使用,通常只存在于如浏览器这样的客户端上。

image-20230710181501724

每一个客户端发起的第一个请求都会被源服务器处理。在缓存生效的情况下,同一个客户端后续的相同请求甚至不会被发送,而是由本地缓存提供服务。

共享缓存:

  • 可以供多个客户端使用的缓存,通常依赖于代理服务器。

image-20230710181738672

客户端发起的第一个请求通过代理服务器访问源服务器,缓存生效后会存放在代理服务器,后续客户端发起的相同请求,均由代理服务器提供缓存服务,共享缓存可以减轻源服务器的压力。

2. HTTP缓存的处理流程

在正式开始之前,我们通过下面这张图通过宏观视角了解下HTTP 缓存的处理流程(执行顺序)。

49218569

39aa830e10cd18056f10546eee30d14

2c8663059564cec57e840f998b47940

3. HTTP缓存策略

首先,我们要知道一点:HTTP的缓存策略,是由客户端和服务器端共同去控制的,客户端可以通过在请求头里添加Cache-Control等字段来决定是否走缓存,服务器端也可以在响应头中添加Cache-Control等字段来告诉客户端是否可以缓存数据。

不管是客户端还是服务端都是提供HTTP响应头中的不同字段来控制的。

3.1 服务器端的缓存控制

  • HTTP响应头中的关于服务器缓存字段说明
    • Expires
    • Cache-Control
    • Last-Modified
    • Etag
  1. Expires

    • Expires表示服务器端告诉客户端当前资源的失效时间,截止到哪个时间点,是一个绝对时间,即过了这个时间点请求的话,就说明缓存已经失效啦,但是由于服务器端时间和客户端时间可能存在偏差,这也就是导致了最后缓存的时间误差,另一方面,该字段是http1.0提出来的,现在我们基本都是用cache-control:max-age:30来替代。
  2. Cache-Control

    • 对于网站来说,缓存是达到高性能的重要组成部分,缓存需要合理配置,因为并不是所有资源都是永久不变的。Cache-Control 首部可以对缓存进行控制,Cache-Control 能用于 HTTP 请求和响应中,支持多个指令,以逗号分隔:
响应首部 描述
Cache-Control: no-store 不使用缓存。
Cache-Control: no-cache 使用缓存前,无论本地副本是否过期,都需要请求源服务器进行验证(协商缓存验证)。
Cache-Control: max-age=秒 设置缓存存储的最大期限,超过这个期限缓存被认为过期,时间是相对于请求的时间。
Cache-Control: s-maxage=秒 max-age,仅适用于共享缓存。
Cache-Control: private 私有缓存,响应只能被单个客户端缓存。
Cache-Control: public 共享缓存,即由缓存代理服务器提供的缓存,响应可以被多个客户端缓存。
Cache-Control: must-revalidate 如果本地副本未过期,则可继续供客户端使用,不需要向源服务器再验证;如果本地副本已过期(比如已经超过max-age),在成功向源服务器验证之前,缓存不能用该资源响应后续请求。
Cache-Control: proxy-revalidate must-revalidate,仅适用于共享缓存。

Cache-Control有几个指令特别容易混淆,不能望文生义。比如no-cache,并不是指不能用 cache,客户端仍会把带有 no-cache 的响应缓存下来,只不过每次不会直接用缓存,而是要先去服务端验证一下,所以其实no-cache真正合适的名字才是 must-revalidate。如果你想让客户端完全不缓存响应,应该用no-store,带有no-store的响应不会被缓存到任意的磁盘或者内存里,它才是真正的 no-cache

==下面是对三个容易混淆的指令进行对比说明:==

首部 描述
Cache-Control: no-store 不使用缓存。
Cache-Control: no-cache 无论本地副本是否过期,都需要请求源服务器进行验证。
Cache-Control: must-revalidate 如果本地副本未过期,可以使用本地副本;否则,需要请求源服务器进行验证。

image-20230710201001928

3.2 客户端的缓存策略

  • 上面我们介绍了,服务器端如何在响应头中添加响应的字段来浏览来是否可以使用缓存,同样,客户端自己也可以控制,即浏览器也可以在请求中添加Cache-Control等字段。

  • 客户端的缓存策略主要依赖以下几种实现:

    • 浏览器的Refresh(刷新)或Reload(重载)按钮;

    (Refresh)我们按F5刷新页面的时候,该页面的http请求中会添加:Cache-Control:max-age:0; 即说明缓存直接失效啦,就不走缓存了,直接从服务器端读取数据。

    (Reload)我们按ctrl+f5强制刷新页面的时候,该页面的http请求会添加:Cache-Control:no-cache; 即表示此时要首先去服务器端验证资源是否有更新,如果有更新则直接返回最新资源,如果没有更新,则返回304,然后浏览器端判断是304的话,则从缓存中读取数据。

    • 浏览器的无痕模式;
    • 浏览器的前进、后退;

    当我们点击浏览器的前进后退操作时,这个时候请求中不会有Cache-Control的字段,没有该字段,就表示会检查缓存,直接利用之前的资源,不再重新请求服务器。

    • 浏览器开发者工具的Disable cache(禁用缓存)

3.2.2 客户端查找缓存的顺序

  1. 先从内存找,如果内存中存在,从内存中加载
  2. 如果内存中没有,那就去硬盘中找,如果硬盘中有,从硬盘中加载;
  3. 如果硬盘没有,就进行网络请求;
  4. 加载到资源缓存到硬盘和内存中。

3.2.3 强制再验证:Pragma:no-cache

Cache-Control: no-cache 效果一致,当响应头中包含该指令时,当客户端再次发起请求时,会强制要求使用缓存之前将请求提交到源服务器进行验证。

Pragma: no-cache 用来向后兼容只支持 HTTP/1.0 协议的缓存服务器。

4. 缓存的新鲜度

4.1 缓存新鲜度概念

在缓存文档过期之前,缓存可以以任意频率使用这些副本,而无需与源服务器联系。当然,除非客户端请求中包含有阻止提供已缓存或未验证资源的首部。一旦已缓存文档过期,缓存就必须与服务器进行核对,询问源服务器该文档是否被修改过,如果被修改过,就要获取一份新鲜(带有新的过期日期)的副本。

4.2 如何检测缓存是否新鲜

使用日期:Cache-Control:max-age=秒

我们可以通过指定一个缓存的最大使用期限,相对于缓存的创建时间,如果超过了最大使用期限,就说明缓存已经不新鲜了。

  • Cache-Control: max-age=秒

例如:

image-20230710204703092

如图所示,当响应头中Cache-Control的max-age设置为10秒时,意味着从第一次请求开始,该资源的缓存有效期是10秒,10秒内再次请求该资源会从缓存中读取;超过10秒,则客户端向源服务器发起请求,缓存的有效期又重新开始计时。

过期日期:Expires

我们还可以通过指定一个绝对的过期日期,如果过期日期已经过了,就说明缓存已经不新鲜了。

Expires:过期日期

tips:

Expires 是 HTTP/1.0 的首部,Cache-Control 是 HTTP/1.1 的首部,Expires 首部和 Cache-Control:max-age 首部所做的事情本质上是一样的,但由于 Cache-Control 首部使用的是相对时间而不是绝对日期,所以更倾向于使用比较新的Cache-Control首部。绝对日期依赖于计算机时钟的正确设置。

5. 服务端再验证

5.1 服务端再验证概念

仅仅是已缓存文档过期了并不意味着它和源服务器上的文档有实际的区别,这只是意味着要和服务器进行核对了,说明缓存需要询问源服务器文档是否发生了变化,这种情况称为“服务器再验证”。

服务端再验证有两种情况:

  • 服务端文档发生了变化:缓存会获取一份新的文档副本,并将其存储在旧文档的位置上,然后将该文档发送给客户端;
  • 服务端文档没有发生变化:缓存只需要获取新的首部,包含一个新的过期日期,并对缓存中的首部进行更新就行了,该文档还可以继续使用。

缓存并不一定要为每条请求验证文档的有效性——只有在文档过期时它才需要与服务器进行再验证。

5.2 如何进行服务端再验证

5.2.1 Last-Modified、If-Modified-Since

Last-ModifiedIf-Modified-Since 的值都是 GMT 格式的时间字符串,代表的是文件的最后修改时间。

  1. 在服务器在响应请求时,会通过Last-Modified告诉浏览器资源的最后修改时间。
  2. 浏览器再次请求服务器的时候,请求头会包含Last-Modified字段,后面跟着在缓存中获得的最后修改时间。
  3. 服务端收到此请求头发现有if-Modified-Since,则与被请求资源的最后修改时间进行对比,如果一致则返回 304 和响应报文头,浏览器只需要从缓存中获取信息即可。如果已经修改,那么开始传输响应一个整体,服务器返回:200 OK

但是在服务器上经常会出现这种情况,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。为了解决这个问题,HTTP/1.1 推出了Etag。Etag 优先级高与Last-Modified

image-20230710210444734

5.2.2 Etag、If-None-Match

Etag都是服务器为每份资源生成的唯一标识,就像一个指纹,资源变化都会导致 ETag 变化,跟最后修改时间没有关系,ETag可以保证每一个资源是唯一的。

在浏览器发起请求,浏览器的请求报文头会包含 If-None-Match 字段,其值为上次返回的Etag发送给服务器,服务器接收到次报文后发现 If-None-Match 则与被请求资源的唯一标识进行对比。如果相同说明资源没有修改,则响应返 304,浏览器直接从缓存中获取数据信息。如果不同则说明资源被改动过,则响应整个资源内容,返回状态码 200。

image-20230710210505087

6. 总结

  1. 首先,浏览器端会根据Cache-Control是否是no-store来判断是否可以对返回的数据进行缓存,如果是no-store表示不允许缓存,之后的请求都不会走缓存,而是重新想服务器端发送请求。
  2. 如果不是no-store,一般就是返回max-age: 5000;来告诉浏览器端可以对数据进行缓存,并且设置缓存的失效时间,通过max-age一般会搭配no-cache或者must-revalidate一起返回,no-cache和must-revalidate就是控制要去服务器端进行验证数据是否真的有变化。
  3. 那如何验证变化呢?就是借助Last-Modified/if-Modified-Since,或者ETag/If-None-Match来判断,如果确实有变化,则返回最新数据,如果没有变化,则返回304,同时更新缓存的失效时间。

以上就是缓存的整个工作机制,其实我们没必要去记忆什么强制缓存,协商缓存等概念,重要的是我们要理解缓存的整个设计思想,每一步的策略到底是解决了什么问题。

参考文章: