HTTP缓存

435 阅读4分钟

什么是HTTP缓存

当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要请求资源”的副本,就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源。

常见的http缓存只能缓存get请求响应的资源,对于其他类型的响应则无能为力。

发送http请求时,发生了什么

http缓存都是从第二次请求开始的。

  • 第一次请求资源时,服务器返回资源,并在response header中加上字段cache-control/expires,Last-Modified/ETag;

  • 第二次请求时,如果有cache-control/expires字段,与客户端时间作对比,判断缓存是否过期,(1)如果没有过期,使用缓存资源,返回200(from disk cache)或200(from memory cache),(2)如果已过期,向服务器发送请求,将上次请求的Last-Modified/Etag一起传递给服务器,与服务器资源作对比,如果没有改变,返回304和一个空的响应体,否则返回200和新的资源

大致的流程如下

为了尽可能地让浏览器从缓存中获取资源,提高缓存的命中率,但同时又要保证被使用的缓存与服务端最新的资源保持一致。需要制定合适的缓存过期策略(简称“缓存策略”),HTTP 支持的缓存策略有两种:强制缓存协商缓存

强制缓存

强制缓存在缓存数据未失效的情况下(即Cache-Control的max-age没有过期或者Expires的缓存时间没有过期),那么就会直接使用浏览器的缓存数据,不会再向服务器发送任何请求。强制缓存生效时,http状态码为200。这种方式页面的加载速度是最快的,性能也是很好的,但是在这期间,如果服务器端的资源修改了,页面上是拿不到的,因为它不会再向服务器发请求了。这种情况就是我们在开发种经常遇到的,比如你修改了页面上的某个样式,在页面上刷新了但没有生效,因为走的是强缓存,所以Ctrl + F5一顿操作之后就好了。

Expires

HTTP/1.0 中可以使用响应头部字段 Expires 来设置缓存时间,它对应一个未来的时间戳。发送请求时,先会对比当前时间和 Expires 对应的时间,如果当前时间早于 Expires 时间,那么直接使用缓存;反之,需要再次发送请求。

expires: Sun, 12 Sep 2021 03:54:37 GMT

但是使用 Expires 响应头时容易产生一个问题,那就是服务端和浏览器的时间很可能不同,因此这个缓存过期时间容易出现偏差。同样的,客户端也可以通过修改系统时间来继续使用缓存或提前让缓存失效。

为了解决这个问题,HTTP/1.1 提出了 Cache-Control 响应头部字段。

Cache-Control

它的常用值有下面几个:

  • no-cache,表示使用协商缓存,即每次使用缓存前必须向服务端确认缓存资源是否更新;

  • no-store,禁止浏览器以及所有中间缓存存储响应内容;

  • public,公有缓存,表示可以被代理服务器缓存,可以被多个用户共享;

  • private,私有缓存,不能被代理服务器缓存,不可以被多个用户共享;

  • max-age,以秒为单位的数值,表示缓存的有效时间;

  • must-revalidate,当缓存过期时,需要去服务端校验缓存的有效性。

需要注意的是,no-cache这个名字有一点误导。设置了no-cache之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致。 no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存

cache-control: public, max-age=31536000

告诉浏览器该缓存为公有缓存,有效期 1 年。

需要注意的是,cache-control 的 max-age 优先级高于 Expires,也就是说如果它们同时出现,浏览器会使用 max-age 的值。

注意,你可能在其他资料中看到可以使用 meta 标签来设置缓存,比如:

<meta http-equiv="expires" content="Wed, 20 Jun 2021 22:33:00 GMT"

但在 HTML5 规范中,并不支持这种方式,所以尽量不要使用 meta 标签来设置缓存。

协商缓存

协商缓存的更新策略是不再指定缓存的有效时间了,而是浏览器直接发送请求到服务端进行确认缓存是否更新,如果请求响应返回的 HTTP 状态为 304,则表示缓存仍然有效。

Last-Modified 和 If-Modified-Since

通过响应头部字段 Last-Modified 和请求头部字段 If-Modified-Since 比对双方资源的修改时间。

大致的流程如下

  • 浏览器第一次请求资源,服务端在返回资源的响应头中加入 Last-Modified 字段,该字段表示这个资源在服务端上的最近修改时间。

  • 当浏览器再次向服务端请求该资源时,请求头部带上之前服务端返回的修改时间,这个请求头叫 If-Modified-Since

  • 服务端再次收到请求,根据请求头 If-Modified-Since 的值,判断相关资源是否有变化,如果没有,则返回 304 Not Modified,并且不返回资源内容,浏览器使用资源缓存值;否则正常返回资源内容200,且更新 Last-Modified 响应头内容。

//response header
last-modified: Sat, 12 Sep 2020 11:54:41 GMT
//request header
If-Modified-Since: Sat, 12 Sep 2020 11:54:41 GMT

这种方式虽然能判断缓存是否失效,但存在两个问题:

  • 精度问题,Last-Modified 的时间精度为秒,如果在 1 秒内发生修改,那么缓存判断可能会失效;

  • 准度问题,考虑这样一种情况,如果一个文件被修改,然后又被还原,内容并没有发生变化,在这种情况下,浏览器的缓存还可以继续使用,但因为修改时间发生变化,也会重新返回重复的内容。

ETag 和 If-None-Match

为了解决精度问题和准度问题,HTTP 提供了另一种不依赖于修改时间,而依赖于文件哈希值的精确判断缓存的方式,那就是HTTP / 1.1响应头部字段 ETag 和请求头部字段 If-None-Match。

大致的流程如下

  • 浏览器第一次请求资源,服务端在返响应头中加入 Etag 字段,Etag 字段值为该资源的哈希值;

  • 当浏览器再次跟服务端请求这个资源时,在请求头上加上 If-None-Match,值为之前响应头部字段 ETag 的值;

  • 服务端再次收到请求,将请求头 If-None-Match 字段的值和响应资源的哈希值进行比对,如果两个值相同,则说明资源没有变化,返回 304 Not Modified;否则就正常返回资源内容200,无论是否发生变化,都会将计算出的哈希值放入响应头部的 ETag 字段中。

这种缓存比较的方式也会存在两个问题:

  • 计算成本。生成哈希值相对于读取文件修改时间而言是一个开销比较大的操作,尤其是对于大文件而言。如果要精确计算则需读取完整的文件内容,如果从性能方面考虑,只读取文件部分内容,又容易判断出错。

  • 计算误差。HTTP 并没有规定哈希值的计算方法,所以不同服务端可能会采用不同的哈希值计算方式。这样带来的问题是,同一个资源,在两台服务端产生的 Etag 可能是不相同的,所以对于使用服务器集群来处理请求的网站来说,使用 Etag 的缓存命中率会有所降低。

需要注意的是,强制缓存的优先级高于协商缓存,在协商缓存中,Etag 优先级比 Last-Modified 高

配置缓存

nginx中配置

禁止静态资源缓存