在正式开始学习之前先来问自己几个问题 🤔️:
- 为什么需要 HTTP 缓存?🌟
- 什么是 HTTP 缓存?🌟🌟
- 缓存的种类有哪些?🌟🌟🌟
- 协商缓存与强缓存是什么?🌟🌟🌟🌟🌟
- Exprie 有什么缺陷?🌟🌟
如果你可以很流畅的答出来并且可以深入任何其一进行展开,那么恭喜你, HTTP 缓存你已经掌握了 🎉,你可以继续往下阅读,如果和本文由不同的见解,欢迎评论指出 🫡
如果你觉得回答这些问题很吃力,也不用担心,跟着本文的脚步,我会逐个问题依次为大家讲解,相信认真读完本文的同学一定会有所收获 🍒
为什么需要 HTTP 缓存
我相信这个问题大家一定有一个共同的认知,那就是“减轻服务器压力”,是的,这样说是没错的,但是,这样的回答并不是那么饱满,别人可能不太懂到底怎样减轻服务器的压力,你可以这样回答:
“在 HTTP 请求与响应交互过程中是一个相当耗时且占用资源的过程,而 HTTP 缓存可以存储与请求相关联的响应,并将存储的响应复用于后续请求中,当响应可复用时,源服务器不需要处理请求——因为它不需要解析和路由请求、根据 cookie 恢复会话、查询数据库以获取结果或渲染模板引擎。这减少了服务器上的负载。”
到这里,你懂了为什么需要 HTTP 缓存,接下来我们一起探究到底什么是 HTTP 缓存。
什么是 HTTP 缓存
HTTP 缓存是响应消息的本地存储和控制其中消息的存储、检索和删除的子系统。缓存存储可缓存的响应,以减少未来等效请求的响应时间和网络带宽消耗。
这里我要指出的是,大家不可浅显的把浏览器缓存和 HTTP 缓存等同,不过你可以将浏览器缓存当作 HTTP 缓存中的一个例子。为什么?大家可以想象这样的一个过程,你打开浏览器输入 www.xxx.com 这样一段网址,然后距离很远的服务器收到了请求,并且这是发出响应返回给你对应的内容。整个过程从哪里做缓存可以让这次响应更快呢?聪明的你肯定想到了,那就是“浏览器”,这也就是浏览器缓存的大概内容。
所以,到这里你应该明白了,HTTP 缓存是整个 HTTP 请求与响应这一过程中把响应内容存储起来的一个技术,而浏览器缓存只是其中之一。🥷
OK,我们现在了解了什么是 HTTP 缓存,接下来看看 HTTP 缓存的种类都有哪些。💃
缓存的种类有哪些
同样是根据 HTTP 缓存标准中的定义,缓存分为私有缓存和共享缓存。
私有缓存
私有缓存是绑定到特定客户端的缓存——通常是浏览器缓存。由于存储的响应不与其他客户端共享,因此私有缓存可以存储该用户的个性化响应。
提到个性化响应你可能会想到 cookie ,⚠️ 注意,虽然 cookie 可以做到个性化响应,但是它并不可以使响应成为私有的。
如果响应包含个性化内容并且你只想将响应存储在私有缓存中,则必须指定 private 指令:
Cache-Control: private
⚠️ 值得注意的是:这种用法仅控制响应的存储位置;不能保证消息内容的私密性。
共享缓存
共享缓存位于客户端和服务器之间,可以存储能在用户之间共享的响应。共享缓存可以进一步细分为代理缓存和托管缓存。
代理缓存
提到代理,大家一定会想到跨域问题可以使用代理来解决,没想到它竟然还可以实现缓存 🥰 的确,这里所说的代理是同一个东西,过去的代理缓存是通过设置一系列缓存控制头来完成,也不需要服务开发人员做什么特殊处理。
Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate
但是,由于 HTTPS 的盛行,代理缓存只作为中间的传输通道而不是作缓存相关的工作了。
托管缓存
与代理缓存不同,托管缓存由服务开发人员明确部署,以降低源服务器负载并有效地交付内容。可以这样简单的理解:托管的意思是托别人管理,那结合起来就是“托别人管理缓存”,具体的手段有反向代理、CDN 和 service worker 与缓存 API 的组合。
下图为整个链路与缓存结合的例子。
本节内容到此为止,我相信你对于 HTTP 缓存已经有了一个大概的认知。
如果对某一个内容感兴趣可以自行去搜索进入更深入的了解。🧐
协商缓存与强缓存
在谈协商缓存和强缓存之前来插点其他的内容,就是启发式缓存。怎么来理解启发式缓存呢?HTTP 缓存规范中提到,由于原始服务器并不总是提供明确的到期时间,因此缓存可以在未指定明确时间时分配启发式缓存到期时间,采用使用其他字段值(例如 Last-Modified 时间)的算法来估计合理的到期时间。该规范没有提供特定的算法,但它确实对它们的结果施加了最坏情况的约束。
通过看下面的例子来对启发式缓存做进一步理解:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Sun, 21 Aug 2022 22:22:22 GMT
Last-Modified: Sat, 21 Aug 2021 22:22:22 GMT
<!doctype html>
…
试探性地知道,整整一年没有更新的内容在那之后的一段时间内不会更新。因此,客户端存储此响应(尽管缺少 max-age)并重用它一段时间。复用多长时间取决于实现,但规范建议存储后大约 10%(在本例中为 0.1 年)的时间。 OK,我们继续回到主题,协商缓存与强缓存,它们的基本原理如下:
- 浏览器在加载资源时,根据请求头的 expires 和 cache-control 判断是否命中强缓存,是则直接从缓存读取资源,不会发请求到服务器。
- 如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过 last-modified 和 etag 验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,依然是从缓存中读取资源
- 如果前面两者都没有命中,直接从服务器加载资源
我们前面提到过私有缓存和共享缓存的概念,那大家来判断下,协商缓存与强制缓存属于哪种呢?我相信聪明的你现在心里一定有答案了~🥳
值得一提的是,协商缓存与强制缓存在 HTTP 规范中并没有定义,可以理解为业界在做浏览器缓存时的一个最佳实践。
强缓存
强缓存是利用了 HTTP 响应的两种状态来做的,分别是 Fresh 和 Stale,Fresh 表示可以直接使用缓存,Stale 则表示缓存的响应已过期。而对于“新鲜度”的定义标准是通过 age 来决定的,在 HTTP 中,age 是自响应生成以来经过的时间。这类似于其他缓存机制中的 TTL。
老规矩上 🌰 (604800 秒是一周):
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Sun, 21 Aug 2022 22:22:22 GMT
Cache-Control: max-age=604800
<!doctype html>
…
通过对于响应状态的理解,上面的示例我们可以这样理解:
- 如果响应的 age 小于一周则为 Fresh
- 如果响应的 age 大于一周则为 Stale
到这里,对于强缓存的解释已经结束了;不对,还没完全结束 :P
有过了解的同学这时候就要发言了,那 Expires
呢,我记得他也是强缓存中的一个啊,很棒 👍
Expires: Sun, 28 Feb 2022 22:22:22 GMT
但是时间格式难以解析,也发现了很多实现的错误,有可能通过故意偏移系统时钟来诱发问题;因此,在 HTTP/1.1 中,Cache-Control 采用了 max-age——用于指定经过的时间。
不过现在人们普遍把 Expires
与 Cache-Control
一起使用:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Sun, 21 Aug 2022 22:22:22 GMT
Expires: Sun, 28 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
<!doctype html>
…
在 HTTP/1.1 中,如果 Expires
和 Cache-Control: max-age
都可用,则将 max-age
定义为首选。
协商缓存
细心的你可能会问,为啥先讲强缓存啊?恭喜你 🎉 ,触发彩蛋 🍬,那是因为默认情况下强缓存的优先级要比协商缓存高。当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的 HTTP 状态码为 304 并且会显示一个 Not Modified 的字符串。
协商缓存其实利用了 HTTP 的一种机制,过时的响应不会立即被丢弃,可以通过询问源服务器将陈旧的响应转换为新的响应。这称为验证,有时也称为重新验证。
验证是通过使用包含 If-Modified-Since
或 If-None-Match
请求标头的条件请求完成的。
而它们分别对应的响应标头为 Last-Modified
与 ETag
,下面我们先来看看 If-Modified-Since
与 Last-Modified
:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Sun, 21 Aug 2022 23:22:22 GMT
Last-Modified: Sun, 21 Aug 2022 22:00:00 GMT
Cache-Control: max-age=3600
<!doctype html>
…
如果内容自指定时间以来没有更改,服务器将响应 304 Not Modified。
HTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600
由于此响应仅表示“没有变化”,并没有响应主体,因此传输大小非常小。
服务器可以从操作系统的文件系统中获取修改时间,这对于提供静态文件的情况来说是比较容易做到的。但是,也存在一些问题;例如,时间格式复杂且难以解析,分布式服务器难以同步文件更新时间。
那么,为了解决这些问题,ETag
响应头作为标准出现了。
ETag
响应头的值是服务器生成的任意值。服务器对于生成值没有任何限制,因此服务器可以根据他们选择的任何方式自由设置值 - 例如正文内容的哈希或版本号。如果资源变化则对应 ETag
发生变化;请求标头 If-None-Match
则是获取到的上一次响应标头 ETag
的值。
如果校验两者的值相同则证明资源未更新,使用缓存,如果两者值不同,则服务器会返回新的资源。
⚠️注意: 在评估如何使用 ETag 和 Last-Modified 时,请考虑以下几点:在缓存重新验证期间,如果 ETag 和 Last-Modified 都存在,则 ETag 优先。因此,如果你只考虑缓存,你可能会认为 Last-Modified 是不必要的。然而,Last-Modified 不仅仅对缓存有用;相反,它是一个标准的 HTTP 标头,内容管理 (CMS) 系统也使用它来显示上次修改时间,由爬虫调整爬取频率,以及用于其他各种目的。所以考虑到整个 HTTP 生态系统,最好同时提供 ETag 和 Last-Modified。
对了,还有一个 Vary
标头没讲,它可以针对用户代理来实现响应式缓存,感兴趣可以自行搜索~🧐
小结
学完这两种缓存策略,我们来梳理一下,首先它们取用的缓存都是浏览器缓存,其次它们的优先级不同,强缓存要更早一些,并且是直接返回缓存内容而协商缓存则是服务器响应 304 状态码。
整体的流程图:
Expries 有什么缺陷
在讲强缓存的时候已经提到了哦~如果你是跳着看的话,你可以返回强缓存查看 💁
总结
你不会以为到这里我们的航海旅程已经结束了吧?哈哈,抖个机灵 :P;不过,HTTP 缓存还有很多细节知识我们没有探索,如果你感兴趣可以继续探索哦~当然,如果有疑问 🤔️ 或者其他的见解欢迎随时与我沟通。
最后,感谢你阅读到这里,如果你觉得有所收获,麻烦你帮我点个赞 💕 ,感谢 🥹