HTTP 缓存机制及原理

Posted by Yezhiwei on June 11, 2020

作者:魑魅魍魉_killer

原文地址:https://www.cnblogs.com/chenlei987/p/11383242.html

1、前言

作为一个前端,了解 HTTP 缓存是非常必要,它不仅是面试的必要环节,也更是实战开发中必不可少需要了解的知识点,本文作者将从缓存的概念讲到如何在业务中设计一个合理的缓存架构,带你一步一步解开 HTTP 缓存的神秘面纱。

2、HTTP 缓存定义

当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要请求资源”的副本,就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源。HTTP 缓存一般针对 GET 请求,POST 请求一般不会缓存,因为 POST 请求执行的任务都是对服务器产生副作用或者非幂等性的任务,既然要改变服务器资源,自然请求是要进入服务器进行处理的。缓存带来的好处是巨大的,减少了 HTTP 请求,自然也就减少的服务端压力,并且增加了资源的访问速度,但是胡乱使用缓存,将会带来资源的不及时更新,甚至资源更新错位,灾难也是巨大的。
HTTP 缓存分为强缓存和协商缓存,下面带大家一一了解两种缓存机制。

3、强缓存

3.1、强缓存定义

如果命中缓存,直接从缓存中拿数据,请求不会经过服务器,返回的 HTTP 状态码为 200(from disk cache)

下面给一张流程图来说明强缓存的请求过程,为了方便假设浏览器存在一个缓存数据库(其实就是disk磁盘,缓存数据存放的地方)。

仔细看上面的流程图,强缓存的最大特点就是在命中缓存的情况下不会经过服务器,而是直接返回。

3.2、HTTP 头 Expires/Cache-Control 设置强缓存

Cache-Control 里面存在多个属性来控制缓存,设置强缓存即设置资源的有效期,属性为 max-age.
下面使用Express给大家演示一下

app.get('/script1.js', function (req, res, next) {
    // res.header('Cache-Control', 'must-revalidate, max-age=600')
    // res.header('Content-Type', 'text/html')
    res.header('Cache-Control', 'max-age=20')
    res.sendFile(__dirname + '/script.js')
})

Expires 和 max-age 都是用于控制缓存的生命周期。不同的是 Expires 指定的是过期的具体时间,例如Sun, 21 Mar 2027 08:52:14 GMT,而 max-age 指定的是生命时长秒数315360000。
区别在于 Expires 是 HTTP/1.0 的中的标准,而 max-age 是属于 Cache-Control 的内容,是 HTTP/1.1 中的定义的。但为了想向前兼容,这两个属性仍然要同时存在。max-age 是要优先于 Expires 的。

4、协商/对比缓存

4.1、定义

协商缓存与强制缓存的不同之处在于,协商缓存每次读取数据时都需要跟服务器通信,并且会增加缓存标识。在第一次请求服务器时,服务器会返回资源,并且返回一个资源的缓存标识,一起存到浏览器的缓存数据库。当第二次请求资源时,浏览器会首先将缓存标识发送给服务器,服务器拿到标识后判断标识是否匹配,如果不匹配,表示资源有更新,服务器会将新数据和新的缓存标识一起返回到浏览器;如果缓存标识匹配,表示资源没有更新,并且返回 304 状态码,浏览器就读取本地缓存服务器中的数据。
协商缓存的最大特点是要经过服务器验证的,下面我们来讲解协商缓存的验证流程。
第一次访问:

再次访问:

还是给一张流程图来说明。(图是盗的,协商缓存也可以成为对比缓存,图中的对比缓存就是协商缓存)

4.2、协商缓存如何验证

第一次请求将 response header 的 Last-Modified 和 Etag 存起来,在第二次请求通过 request header 的 If-Modified-Since 和If-None-Match 传到服务端进行验证,如果命中缓存,返回 304,不带返回的数据,浏览器自动从缓存中获取数据资源,若未命中缓存返回 200,带上数据资源。

** Last-Modified:**
服务器在响应请求时,告诉浏览器资源的最后修改时间。

** If-Modified-Since:**
再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间。
服务器收到请求后发现有头 If-Modified-Since 则与被请求资源的最后修改时间进行比对。
若资源的最后修改时间大于 If-Modified-Since,说明资源又被改动过,则响应整片资源内容,返回状态码 200;
若资源的最后修改时间小于或等于 If-Modified-Since,说明资源无新修改,则响应 HTTP 304,告知浏览器继续使用所保存的 cache。

** Etag / If-None-Match(优先级高于Last-Modified / If-Modified-Since) **
** Etag:**
服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。

** If-None-Match:**
再次请求服务器时,通过此字段通知服务器客户段缓存数据的唯一标识。
服务器收到请求后发现有头 If-None-Match 则与被请求资源的唯一标识进行比对,
不同,说明资源又被改动过,则响应整片资源内容,返回状态码200;
相同,说明资源无新修改,则响应 HTTP 304,告知浏览器继续使用所保存的 cache。

4.3、HTTP 头如何设置协商缓存

在强缓存那一节说到使用 Cache-Control 的 max-age 来设置资源过期时间,那么当 max-age=0 的时候呢,自然浏览器第一时间发现资源过期,request header 就会带着 If-Modified-Since 和I f-None-Match 去服务端验证。
所以设置 response header 为:

Cache-Control: max-age=0

就可以触发协商缓存了,其实 Cache-Control 中还有两个属性都可以设置协商缓存 must-revalidate 和 no-cache
must-revalidate 的意义为必须进行验证,但是它一般是和 max-age一起使用的,不会单独使用,

Cache-Control: must-revalidate, max-age=600

该头信息意义就是在资源有效期过后必须进行验证, 与只设置 max-age=600 的区别是,前面一个是 MUST,而后面一个是 SHOULD,理论上来说它们的效果是一致的。
no-cache 的意义千万不能理解为不缓存,下面两段代码的意义是一样的,即请求必须进行验证,才可以使用缓存资源,注意是 MUST

Cache-Control: no-cache
Cache-Control: must-revalidate, max-age=0

如果要不缓存,每次都请求新的资源应该使用

Cache-Control: no-store

5、关于缓存的 HTTP 头总结

5.1、"no-cache", "no-store", "must-revalidate"

Cache-Control 字段可以设置的不仅仅是 max-age 存储时间,还有其他额外的值可以填写,甚至可以组合。主要使用的值有如下:
no-cache: 虽然字面意义是“不要缓存”。但它实际上的机制是,仍然对资源使用缓存,但每一次在使用缓存之前必须(MUST)向服务器对缓存资源进行验证。
no-store: 不使用任何缓存
must-revalidate: 如果你配置了 max-age 信息,当缓存资源仍然新鲜(小于max-age)时使用缓存,否则需要对资源进行验证。所以 must-revalidate 可以和 max-age 组合使用 Cache-Control: must-revalidate, max-age=60
有趣的事情是,虽然 no-cache 意为对缓存进行验证,但是因为大家广泛的错误的把它当作 no-store 来使用,所以有的浏览器也就附和了这种设计。这是一个典型的劣币驱逐良币。

5.2、Expires VS. max-age

Expires 和 max-age 都是用于控制缓存的生命周期。不同的是 Expires 指定的是过期的具体时间,例如Sun, 21 Mar 2027 08:52:14 GMT,而 max-age 指定的是生命时长秒数315360000。
区别在于 Expires 是 HTTP/1.0 的中的标准,而 max-age 是属于Cache-Control 的内容,是 HTTP/1.1 中的定义的。但为了想向前兼容,这两个属性仍然要同时存在。
但有一种更倾向于使用 max-age 的观点认为 Expires 过于复杂了。例如上面的例子Sun, 21 Mar 2027 08:52:14 GMT,如果你在表示小时的数字缺少了一个0,则很有可能出现出错;如果日期没有转换到用户的正确时区,则有可能出错。这里出错的意思可能包括但不限于缓存失效、缓存生命周期出错等。

5.3、Etag VS. Last-Modified

Etag和Last-Modified 都可以用于对资源进行验证,而 Last-Modified 顾名思义,表示资源最后的更新时间。
我们把这两者都成为验证器(Validators),不同的是,Etag 属于强验证(Strong Validation),因为它期望的是资源字节级别的一致;而 Last-Modified 属于弱验证(Weak Validation),只要资源的主要内容一致即可,允许例如页底的广告,页脚不同。
根据 RFC 2616 标准中的13.3.4小节,一个使用 HTTP 1.1 标准的服务端应该(SHOULD)同时发送 Etag 和 Last-Modified 字段。同时一个支持 HTTP 1.1 的客户端,比如浏览器,如果服务端有提供 Etag 的话,必须(MUST)首先对 Etag 进行 Conditional Request(If-None-Match头信息);如果两者都有提供,那么应该(SHOULD)同时对两者进行 Conditional Request(If-Modified-Since头信息)。如果服务端对两者的验证结果不一致,例如通过一个条件判断资源发生了更改,而另一个判定资源没有发生更改,则不允许返回 304 状态。但话说回来,是否返回还是通过服务端编写的实际代码决定的。所以仍然有操纵的空间。

5.4、max-age=0 VS. no-cache

max-age=0 是在告诉浏览器,资源已经过期了,你应该(SHOULD)对资源进行重新验证了;而 no-cache 则是告诉浏览器在每一次使用缓存之前,你必须(MUST)对资源进行重新验证。
区别在于,SHOULD 是非强制性的,而 MUST 是强制性的。在 no-cache 的情况下,浏览器在向服务器验证成功之前绝不会使用过期的缓存资源,而 max-age=0 则不一定了。虽然理论上来说它们的效果应该是一致的。

5.5、public VS. private

要知道从服务器到浏览器之间并非只有浏览器能够对资源进行缓存,服务器的返回可能会经过一些中间(intermediate)服务器甚至甚至专业的中间缓存服务器,还有 CDN。而有些请求返回是用户级别、是私人的,所以你可能不希望这些中间服务器缓存返回。此时你需要将 Cache-Control 设置为 private 以避免暴露。

6、缓存实战

前面写了很多缓存的基础知识,那么如何设计一个可靠的缓存规则,这个其实得根据你的实际需求而定。
比如某个资源永远不会改变,比如某些第三方库(一般都放 CDN 做优化了),或者某些图片,比如百度的图片,就让它们永久缓存着吧,设置一个最大的 max-age

Cache-Control: max-age=31536000

其他的资源可根据下面这张决策树来进行设置

7、memory cache

memory cache 叫做内存缓存,根据操作系统的常理,先读内存,后读硬盘。内存存储较小,一般只存储临时性文件。

“内存缓存”中主要包含的是当前文档中页面中已经抓取到的资源(预请求资源)。例如页面上已经下载的样式、脚本、图片等。我们不排除页面可能会对这些资源再次发出请求,所以这些资源都暂存在内存中,当用户结束浏览网页并且关闭网页时,内存缓存的资源会被释放掉。
这其中最重要的缓存资源其实是 preloader 相关指令(例如)下载的资源。总所周知 preloader 的相关指令已经是页面优化的常见手段之一,而通过这些指令下载的资源也都会暂存到内存中。根据一些材料,如果资源已经存在于缓存中,则可能不会再进行 preload。
memory cache 机制保证了一个页面中如果有两个相同的请求 (例如两个 src 相同的 ,两个 href 相同的 )都实际只会被请求最多一次,避免浪费。
需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的 HTTP 缓存头 Cache-Control 是什么值,同时资源的匹配也并非仅仅是对 URL 做匹配,还可能会对 Content-Type,CORS 等其他特征做校验
这个应该是浏览器做的一种优化,缓存也只是暂时的。

8、浏览器可能会限制 Cache-Control 头无效

参考文章

https://zhuanlan.zhihu.com/p/28113197#comments
https://www.cnblogs.com/chenqf/p/6386163.html

如果觉得还有帮助的话,你的关注和转发是对我最大的支持,O(∩_∩)O: