设计一个完美的http缓存策略

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和If-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头无效

** 参考文章: **
http://zhuanlan.zhihu.com/p/28113197#comments
http://urgr.cn/chenqf/p/6386163.html

posted @ 2019-08-20 15:48 魑魅魍魉_killer 阅读(...) 评论(...) 编辑 收藏