缓存的基本原理

所谓 客户端缓存 是将某一次的响应结果保存在客户端(比如浏览器)中,而后续的请求仅需要从缓存中读取即可

  1. 极大的降低了服务器的处理压力,大大提升了网站的性能
  2. 减少了冗余的数据传输
  3. 加快了客户端加载网页的速度
  1. 客户端缓存的原理如下:

  2. 这里就涉及到一个缓存策略的问题,这些问题包括:

    1. 哪些资源需要加入到缓存,哪些不需要?
    2. 缓存的时间是多久呢?
    3. 如果服务器的资源有改动,客户端如何更新缓存呢?
    4. 如果缓存过期了,可是服务器上的资源并没有发生变动,又该如何处理呢?
  3. 缓存的资源去哪里了?

    memory cache disk cache
    相同点 只能存储一些派生类资源文件 只能存储一些派生类资源文件
    不同点 退出进程时数据会被清除 退出进程时数据不会被清除
    存储资源 一般脚本、字体、图片会存在内存当中 一般非脚本会存在内存当中,如 css

缓存的分类

  1. 强缓存:强缓存指的是在缓存时间内不会向服务器发起请求,只有过期之后才会向服务器发起请求,在浏览器中,强缓存分为 Expires (http1.0 规范)cache-control (http1.1 规范) 两种;

  2. 协商缓存:协商缓存都会向服务器发送请求,判断缓存数据是否过期,过期的话会返回新的内容,没有过期则使用本地的缓存数据,对于协商缓存主要利用两个字段:Last-ModifyEtag

来自服务器的缓存指令

  1. 当客户端发出一个 GET /index.js 请求到服务器,服务器在 响应头 中加入了以下内容,通过响应头传递给客户端了:

    # 把这个资源缓存起来,缓存时间是3600秒(1小时)
    Cache-Control: max-age=3600
    
    # 资源的编号是 W/"121-171ca289ebf",根据内容生成 ETag
    ETag: W/"121-171ca289ebf"
    
    # 响应这个资源的服务器时间(格林威治时间)
    Date: Thu, 30 Apr 2020 12:39:56 GMT
    
    # 上一次修改时间(格林威治时间)
    Last-Modified: Thu, 30 Apr 2020 08:16:31 GMT
    
  2. 如果客户端是其他应用程序,根本不会缓存任何东西,但若客户端是一个浏览器会做如下记录:

    • 浏览器把这次请求得到的 响应体 缓存到本地文件中
    • 浏览器标记这次请求的 请求方法请求路径
    • 浏览器标记这次 缓存的时间3600
    • 浏览器记录 服务器的响应时间 是格林威治时间 2020-04-30 12:39:56
    • 浏览器记录 服务器给予的资源编号W/“121-171ca289ebf”
    • 浏览器记录 资源的上一次修改时间 是格林威治时间 2020-04-30 08:16:31
  3. 这一次的记录非常重要,它为以后浏览器要不要去请求服务器提供了各种依据:

来自客户端的缓存指令

  1. 当客户端准备再次请求 GET /index.js 时,会到缓存中去寻找是否有缓存的资源,寻找的过程如下:

    • 缓存中是否有匹配的请求方法和路径?
    • 如果有,该缓存资源是否还有效呢?
  2. 验证是否有匹配的缓存:只需要验证当前的请求方法 GET 和当前的请求路径 /index.js 是否有对应的缓存存在即可,如果没有,就直接请求服务器,就和第一次请求服务器时一样;

  3. 验证缓存是否有效:就是把 max-age + Date 得到一个过期时间,看看这个过期时间是否大于当前时间,如果是,则表示缓存还没有过期,仍然有效,如果不是,则表示缓存失效;

缓存有效

  1. 当浏览器发现缓存有效时,完全不会请求服务器,直接使用缓存即可得到结果;

  2. 此时,如果断开网络,会发现资源仍然可用,这种情况会极大的降低服务器压力,但当服务器更改了资源后,浏览器是不知道的,只要缓存有效,它就会直接使用缓存;

缓存无效

  1. 当浏览器发现缓存已经过期,它 并不会简单的把缓存删除,而是抱着一丝希望,想问问服务器,我这个缓存还能继续使用吗

  2. 于是,浏览器向服务器发出了一个 带缓存的请求,加入了以下的请求头:

    # 这个资源的上一次修改时间是格林威治时间`2020-04-30 08:16:31`,请问这个资源在这个时间之后有发生变动吗?
    If-Modified-Since: Thu, 30 Apr 2020 08:16:31 GMT
    
    # 这个资源的上一次编号是 W/"121-171ca289ebf",请问这个资源的编号发生变动了吗?
    If-None-Match: W/"121-171ca289ebf"
    
  3. 之所以要发两个信息,是为了兼容不同的服务器,因为有些服务器只认 If-Modified-Since,有些服务器只认 If-None-Match,有些服务器两个都认;

    • 目前的很多服务器,只要发现 If-None-Match 存在,就不会去看 If-Modified-Since
    • If-Modified-Sincehttp1.0 版本的规范,If-None-Matchhttp1.1 的规范
  4. 此时,问题又抛给了服务器,服务器可能会产生两个情况:

    • 缓存已经失效:服务器再次给予一个正常的响应(响应码 200 带响应体),响应头带上新的缓存指令,这就回到了上面——来自服务器的缓存指令,客户端就会重新缓存新的内容;
    • 缓存仍然有效:它可以通过一种极其简单的方式告诉客户端:(304 Not Modified、无响应体),响应头带上新的缓存指令,见上面——来自服务器的缓存指令,就相当于告诉客户端:「你的缓存资源仍然可用,我给你一个新的缓存时间,你那边更新一下就可以了」

细节

Cache-Control

  1. 在上述的讲解中,Cache-Control 是服务器向客户端响应的一个消息头,它提供了一个 max-age 用于指定缓存时间,实际上 Cache-Control 还可以设置下面一个或多个值:

    • public:指示服务器资源是公开的。比如有一个页面资源,所有人看到的都是一样的。这个值对于浏览器而言没有什么意义,但可能在某些场景可能有用。本着「我告知,你随意」的原则,http 协议中很多时候都是客户端或服务器告诉另一端详细的信息,至于另一端用不用,完全看它自己;
    • private:指示服务器资源是私有的。比如有一个页面资源,每个用户看到的都不一样。这个值对于浏览器而言没有什么意义,但可能在某些场景可能有用。本着「我告知,你随意」的原则,http 协议中很多时候都是客户端或服务器告诉另一端详细的信息,至于另一端用不用,完全看它自己;
    • no-cache:告知客户端,你可以缓存这个资源,但是不要 直接 使用它。当你缓存之后,后续的每一次请求都需要附带缓存指令,让服务器告诉你这个资源有没有过期;见:「来自客户端的缓存指令 - 缓存无效」
    • no-store:告知客户端,不要对这个资源做任何的缓存,之后的每一次请求都按照正常的普通请求进行。若设置了这个值,浏览器将不会对该资源做出任何的缓存处理;
    • max-age:不再赘述;
  2. 比如,Cache-Control: public, max-age=3600 表示这是一个公开资源,请缓存 1 个小时;

Expire

  1. http1.0 版本中通过 Expire 响应头来指定过期时间点的,是一个绝对的时间 (当前时间+缓存时间),例如:

    Expires: Thu, 30 Apr 2020 23:38:38 GMT
    
  2. 在响应消息头中,设置这个字段之后,就可以告诉浏览器,在未过期之前不需要再次请求,但是,这个字段设置时有两个缺点:

    • 由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源;此外,即使不考虑自行修改的因素,时差或者误差等因素也可能造成客户端与服务端的时间不一致,致使缓存失效;
    • 写法太复杂了;表示时间的字符串多个空格,少个字母,都会导致变为非法属性从而设置失效;
  3. 到了 http1.1 版本通过 Cache-Controlmax-age 来记录了;

Last-Modified、If-Modified-Since

  1. 服务器通过 Last-Modified 字段告知客户端,资源最后一次被修改的时间,例如:

    Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
    
  2. 浏览器将这个值和内容一起记录在缓存数据库中;

  3. 下一次请求相同资源时时,浏览器从自己的缓存中找出 不确定是否过期的 缓存;因此在请求头中将上次的 Last-Modified 的值写入到请求头的 If-Modified-Since 字段;

  4. 服务器会将 If-Modified-Since 的值与 Last-Modified 字段进行对比;如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据;

  5. 但是他还是有一定缺陷的:

    • 如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒;
    • 如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用;
  6. 因此在 HTTP/1.1 出现了 ETagIf-None-Match

Etag、If-None-Match

  1. 为了解决上述问题,出现了一组新的字段 EtagIf-None-Match

    • Etag 存储的是文件的特殊标识(一般都是一个 Hash 值),服务器存储着文件的 Etag 字段;
    • 之后的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件 hash ,把 If-Modified-Since 变成了 If-None-Match
  2. 浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到请求头里的 If-None-Match 里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好地判断资源相对客户端而言是否被修改过了;

  3. 如果服务器发现 ETag 匹配不上,那么直接以常规 GET 200 回包形式将新的资源(当然也包括了新的 ETag)发给客户端;如果 ETag 是一致的,则直接返回 304 告诉客户端直接使用本地缓存即可;

  4. 两者之间的简单对比:

    • 首先在精确度上 Etag 要优于 Last-ModifiedLast-Modified 的时间单位是秒,如果某个文件在 1 秒内改变了多次,那么 Last-Modified 其实并没有体现出来修改,但是 Etag 是一个 Hash 值,每次都会改变从而确保了精度;
    • 第二在性能上 Etag 要逊于 Last-Modified ,毕竟 Last-Modified 只需要记录时间,而 Etag 需要服务器通过算法来计算出一个 Hash 值;
    • 第三在优先级上服务器校验优先考虑 Etag ,也就是说 Etag 的优先级高于 Last-Modified

记录缓存有效期

  1. 浏览器会按照服务器响应头的要求,自动记录缓存到本地文件,并设置各种相关信息,在这些信息中,有效期 尤为关键,它决定了这个缓存可以使用多久,浏览器会根据服务器不同的响应情况,设置不同的有效期;

  2. 具体的有效期设置,按照下面的流程进行:

  3. 例如,当 max-age = 0 时,缓存立即过期,虽然立即过期,但缓存仍然被记录下来,后续的请求通过缓存指令发送到服务器,来确认资源是否被更改;因此,Cache-Control: max-age=0 类似于 Cache-Control: no-cache

Pragma

  1. 这是 http1.0 版本的消息头,当该消息头出现在请求中时,是向服务器表达:不要考虑任何缓存,给我一个正常的结果;

  2. http1.1 版本中,可以在 请求头 中加入 Cache-Control: no-cache 实现同样的含义;

  3. Chrome 浏览器中调试时,如果勾选了 Disable cache ,则发送的请求中会附带该信息

Vary

  1. 有的时候,是否有缓存,不仅仅是判断请求方法和请求路径是否匹配,可能还要判断头部信息是否匹配,此时就可以使用 Vary 字段来指定要区分的消息头,比如,当使用GET /personal.html 请求服务器时,请求头中 cookie 的值不一样,得到的页面也不一样;

  2. 如果还按照之前的做法,仅仅匹配请求方法和请求路径,如果 cookie 变动,可能得到的仍然是之前的页面,正确的做法如下:

使用版本号或 hash

  1. 如果使用过 vue 或其他基于 webpack 搭建的工程,会发现打包的结果中很多文件名类似于这样 app.68297cd8.css,文件的中间部分使用了 hash 值,这样做的好处是,可以让客户端大胆的、长时间的缓存该文件,减轻服务器的压力,当文件改动后,它的文件 hash 值也会随之而变,这样一来,客户端要请求新的文件时,就会发现路径从 /app.68297cd8.css 变成了 app.446fccb8.css,由于之前的缓存路径无法匹配到,因此就会发送新的请求来获取新资源了;

  2. 而在古老的年代,没有构建工具出现时,使用的办法是在资源路径后面加入版本号来获取新版本的文件,比如:

    1. 页面中引入了一个 css 资源 app.css,它可能的引入方式是:<link href=“/app.css?v=1.0.0”>,这样一来,缓存的路径是 /app.css?v=1.0.0
    2. 当服务器的版本发生变化时,可以给予新的版本号,让 html 中的路径发生变动 <link href=“/app.css?v=1.0.1”>,由于新的路径无法命中缓存,于是浏览器就会发送新的普通请求来获取这个资源;

总结

  1. 服务器无法知道客户端到底有没有像浏览器那样缓存文件,它只管根据请求的情况来决定如何响应,很多后端语言搭建的服务器都会自带自己的默认缓存规则,当然也支持不同程度的修改;

  2. 浏览器在发出请求时会判断要不要使用缓存

  3. 当收到服务器响应时,会自动根据缓存指令进行处理

面试题

为什么用多个域名存储网站资源更有效?

  1. 主要原因是浏览器对同一个域下的 TCP 连接数是有限制的,这样就导致某个网页如果外部资源多了,比如图片很多的网页,在解析页面时,由于 TCP 连接数受限,就无法同时发起多个下载连接,无法充分利用带宽资源;

  2. 因此,可以把静态资源放到多个域名下,这样就绕开了连接数的限制,做到了并发下载;

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 缓存的基本原理
  2. 2. 缓存的分类
  3. 3. 来自服务器的缓存指令
  4. 4. 来自客户端的缓存指令
    1. 4.1. 缓存有效
    2. 4.2. 缓存无效
  5. 5. 细节
    1. 5.1. Cache-Control
    2. 5.2. Expire
    3. 5.3. Last-Modified、If-Modified-Since
    4. 5.4. Etag、If-None-Match
    5. 5.5. 记录缓存有效期
    6. 5.6. Pragma
    7. 5.7. Vary
    8. 5.8. 使用版本号或 hash
  6. 6. 总结
  7. 7. 面试题
    1. 7.1. 为什么用多个域名存储网站资源更有效?