缓存技术之HTTP缓存-协商缓存

143 阅读6分钟

这是我参与8月更文挑战的第16天,活动详情查看: 8月更文挑战

前言

HTTP缓存应该算是前端开发中最常接触的缓存机制之一,它又可细分为强制缓存与协商缓存,二者最大的区别在于判断缓存命中时,浏览器是否需要向服务器端进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求。

协商缓存

顾名思义,协商缓存就是在使用本地缓存之前,需要向服务器端发起一次GET请求,与之协商当前浏览器保存的本地缓存是否已经过期。
通常是采用所请求资源最近一次的修改时间戳来判断的,为了便于理解,下面来看一个例子:假设客户端浏览器需要向服务器请求一个manifest.jss的JavaScript文件资源,为了让该资源被再次请求时能通过协商缓存的机制使用本地缓存,那么首次返回该图片资源的响应头中应包含一个名为last-modified的字段,该字段的属性值为该JavaScript文件最近一次修改的时间戳,简略截取请求头与响应头的关键信息如下:

// 资源的首次请求头
:authority: ss1.example.com
:method: GET
:path: /dist/manifest.js
:scheme: https
// 资源的首次响应头
last-modified: Sun, 16 Aug 2021 21:39:28 GMT
status: 200
content-type: application/javascript

当我们按F5键或按Ctri+R组合键刷新网页时,由于该JavaScript文件使用的是协商缓存,客户端浏览器无法确定本地缓存是否过期,所以需要向服务器发送一次GET请求,进行缓存有效性的协商,此次GET请求的请求头中需要包含一个if- modified-since字段,其值正是上次响应头中last-modified的字段值。
当服务器收到该请求后便会对比请求资源当前的修改时间戳与if-modiffed-since。字段的值,如果二者相同则说明缓存未过期,可继续使用本地缓存,否则服务器重新返回全新的文件资源,简略截取请求头的关键信息如下:

// 再次请求的请求头
:authority: ss1.example.com
:method: GET
:path: /dist/manifest.js
:scheme: https
if-modified-since: Sun, 16 Aug 2021 21:39:28 GMT
// 协商缓存有效的响应头
status code: 304 not modified

这里需要注意的是,协商缓存判断缓存有效的响应状态码是304,即缓存有效响应重定向到本地缓存上。这和强制缓存有所不同,强制缓存若有效,则再次请求的响应状态码是200。

last-modified的不足

通过last-modified所实现的协商缓存能够满足大部分的使用场景,但也存在两个比较明显的缺陷:首先它只是根据资源最后的修改时间戳进行判断的,虽然请求的文件资源进行了编辑,但内容并没有发生任何变化,时间戳也会更新,从而导致协商缓存时关于有效性的判断验证为失效,需要重新进行完整的资源请求。
这无疑会造成网络带宽资源的浪费,以及延长用户获取到目标资源的时间。其次标识文件资源修改的时间戳单位是秒,如果文件修改的速度非常快,假设在几百毫秒内完成,那么上述通过时间戳的方式来验证缓存的有效性,是无法识别出该次文件资源的更新的。
其实造成上述两种缺陷的原因相同,就是服务器无法仅依据资源修改的时间戳来识别出真正的更新,进而导致重新发起了请求,该重新请求却使用了缓存的Bug场景。

基于ETag的协商缓存

为了弥补通过时间戳判断的不足,从HTTP1.1规范开始新增了一个ETag的头信息,即实体标签(Entity Tag)。其内容主要是服务器为不同资源进行哈希运算所生成的一个字符串,该字符串类似于文件指纹,只要文件内容编码存在差异,对应的ETag标签值就会不同,因此可以使用ETag对文件资源进行更精准的变化感知。下面我们来看一个使用ETag进行协商
缓存图片资源的示例,首次请求后的部分响应头关键信息如下:

// 响应头
content-Type: image/jpeg
ETag: "5cbee66d-264"
last-modified: Sun, 16 Aug 2021 21:39:28 GMT
Content-Length: 9887

上述响应头中同时包含了last-modified文件修改时间戳和ETag实体标签两种协商缓存的有效性校验字段,因为ETag比last-modified具有更准确的文件资源变化感知,所以它的优先级也更高,二者同时存在时以ETag为准。再次对该图片资源发起请求时,会将之前响应头中ETag的字段值作为此次请求头中if-None-Match字段,提供给服务器进行缓存有效性验证。请求头与响应头的关键字段信息如下:

// 再次请求头
If-Modified-Sine: Sun, 16 Aug 2021 21:39:28 GMT
If-None-Match: "5cbee66d-264"
// 再次响应头
Content-Type: image/jpeg
ETag: "5cbee66d-264"
Last-Modified: Sun, 16 Aug 2021 21:39:28 GMT
Content-Length: 0

若验证缓存有效,则返回304状态码相应重定向到本地缓存,所以上面响应头中的内容长度Content-Length字段值也就为0了

Etag的不足

不像强制缓存中cache-control可以完全替代expires的功能,在协商缓存中,ETag并非last-modified的替代方案而是一种补充方案,因为它依旧存在一些弊端,一方面服务器对于生成文件资源的ETag需要付出额外的计算开销,如果资源的尺寸较大,数量较多且修改比较频繁,那么生成ETag的过程就会影响服务器的性能。
另一方面ETag字段值的生成分为强验证和弱验证,强验证根据资源内容进行生成,能够保证每个字节都相同;弱验证则根据资源的部分属性值来生成,生成速度快但无法确保每个字节都相同,并且在服务器集群场景下,也会因为不够准确而降低协商缓存有效性验证的成功率,所以恰当的方式是根据具体的资源使用场景选择恰当的缓存校验方式。