Web 缓存

Web 缓存可以分为这几种:

  1. 浏览器缓存
  2. CDN缓存
  3. 服务器缓存
  4. 数据库数据缓存

因为可能会直接使用副本免于重新发送请求或者仅仅确认资源没变无需重新传输资源实体,Web 缓存可以减少延迟加快网页打开速度、重复利用资源减少网络带宽消耗、降低请求次数或者减少传输内容从而减轻服务器压力。

本文介绍浏览器缓存,也即 HTTP 缓存,具体包括强缓存和协商缓存。 HTTP Cache - 图1 强缓存和协商缓存最大也是最根本的区别是:强缓存命中的话不会发请求到服务器(比如 chrome 中的 200 from memory cache),协商缓存一定会发请求到服务器,通过资源的请求首部字段验证资源是否命中协商缓存,如果协商缓存命中,服务器会将这个请求返回,但是不会返回这个资源的实体,而是通知客户端可以从缓存中加载这个资源(304 not modified)。

image.png

强缓存

当缓存数据库中已有所请求的数据时。客户端直接从缓存数据库中获取数据。当缓存数据库中没有所请求的数据时,客户端的才会从服务端获取数据。

强缓存具体和以下两个 Header 字段有关:

Expires

  • Expires:响应头,代表该资源的过期时间。

服务器和浏览器约定文件过期时间,用 Expires 字段来控制,时间是 GMT 格式的标准时间,如 Fri, 01 Jan 1990 00:00:00 GMT,Exprires 的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差,另一方面,Expires 是 HTTP/1.0 的产物,故现在大多数使用 Cache-Control 替代,当 Expires 和 Cache-Control 同时存在时,前者不生效。

例如:Expires: Mon, 26 Sep 2018 05:00:00 GMT

此处相当于服务器告诉浏览器:你把我发给你的 xxx.js 文件缓存到你那里,在 2018年9月26日5点之前不要再发请求烦我,直接使用你自己缓存的 xxx.js 就行了。

优点:

  • 在过期时间以内,为用户省了很多流量。
  • 减少了服务器重复读取磁盘文件的压力。
  • 缓存过期后,能够得到最新的 xxx.js 文件。

缺点:

  • 缓存过期以后,服务器不管 xxx.js有没有变化,都会再次读取 xxx.js文件,并返给浏览器,不够灵活。

Cache-Control

  • Cache-Control:请求/响应头,缓存控制字段,精确控制缓存策略。

为了兼容已经实现了协商缓存方案的浏览器,同时加入新的缓存方案,服务器除了告诉浏览器 Expires ,同时告诉浏览器一个相对时间 Cache-Control:max-age=10。意思是在10秒以内,使用缓存到浏览器的 xxx.js 资源。

浏览器先检查 Cache-Control,如果有,则以 Cache-Control 为准,忽略 Expires。如果没有 Cache-Control,则以 Expires 为准。

Cache-Control 有很多属性,不同的属性代表的意义也不同:

  • private:资源不允许被中间代理服务器缓存
  • public:客户端和代理服务器都可以缓存
  • max-age=t:缓存内容将在t秒后失效
  • no-cache:需要使用协商缓存来验证缓存数据,每次访问资源,浏览器都要向服务器询问,如果文件没变化,服务器只告诉浏览器继续使用缓存(304)。
  • no-store:所有内容都不会缓存,每次访问资源,浏览器都必须请求服务器,并且,服务器不去检查文件是否变化,而是直接返回完整的资源。
  • must-revalidate,可以缓存,但是使用之前必须先向源服务器确认。
  • proxy-revalidate,要求缓存服务器针对缓存资源向源服务器进行确认。
  • s-maxage:缓存服务器对资源缓存的最大时间。

协商缓存

又称对比缓存,客户端会先从缓存数据库中获取到一个缓存数据的标识,得到标识后请求服务端验证是否失效(新鲜),如果没有失效服务端会返回304,此时客户端直接从缓存中获取所请求的数据,如果标识失效,服务端会返回更新后的数据。

协商缓存具体和以下两组 Header 字段有关:

Last-Modified & If-Modified-Since

  • Last-Modified:响应头,资源最近修改时间,由服务器告诉浏览器。
  • If-Modified-Since:请求头,资源最近修改时间,由浏览器告诉服务器。

为了解决强缓存方案的问题,服务器和浏览器经过磋商,制定了一种方案,服务器每次返回 xxx.js 的时候,还要告诉浏览器 xxx.js 在服务器上的最近修改时间 Last-Modified (GMT标准格式)。

假设 xxx.js 大小 10kb,HTTP 请求头和响应头各 1kb。

  • 浏览器访问 xxx.js 文件。(1KB)
  • 服务器返回 xxx.js 的时候,告诉浏览器 xxx.js 文件。(10+1=11KB)
    在服务器的上次修改时间 Last-Modified(GMT标准格式)以及缓存过期时间 Expires(GMT标准格式)
  • 当 xxx.js 过期时,浏览器带上 If-Modified-Since(等于上一次请求的 Last-Modified) 请求服务器。(1KB)
  • 服务器比较请求头里的 Last-Modified 时间和服务器上 a.js的上次修改时间:
    • 如果一致,则告诉浏览器:你可以继续用本地缓存(304)。
      此时,服务器不再返回 xxx.js 文件。(1KB)
    • 如果不一致,服务器读取磁盘上的 xxx.js 文件返给浏览器,同时告诉浏览器 xxx.js 的最近的修改时间 Last-Modified 以及过期时间 Expires。(1+10=11KB)
    • 如此往复。

优点:更加灵活

  • 缓存过期后,服务器检测如果文件没变化,不再把 xxx.js发给浏览器,省去了 10KB 的流量。
  • 缓存过期后,服务器检测文件有变化,则把最新的 xxx.js 发给浏览器,浏览器能够得到最新的 xxx.js。

缺点:

  • Expires 过期控制不稳定,因为浏览器端可以随意修改时间,导致缓存使用不精准。
  • Last-Modified 过期时间只能精确到秒。

Etag & If-None-Match

  • Etag:响应头,资源标识,由服务器告诉浏览器。
  • If-None-Match:请求头,缓存资源标识,由浏览器告诉服务器。

为了解决文件修改时间只能精确到秒带来的问题,我们给服务器引入 Etag 响应头,xxx.js 内容变了,Etag 才变。内容不变,Etag 不变,可以理解为 Etag 是文件内容的唯一 ID。 同时引入对应的请求头 If-None-Match,每次浏览器请求服务器的时候,都带上 If-None-Match 字段,该字段的值就是上次请求 xxx.js 时,服务器返回给浏览器的 Etag。

image.png

  • 浏览器请求 xxx.js。
  • 服务器返回 xxx.js,同时告诉浏览器过期绝对时间(Expires)以及相对时间(Cache-Control:max-age=10),以及 xxx.js 上次修改时间Last-Modified,以及 xxx.js 的 Etag。
  • 10秒内浏览器再次请求 xxx.js,不再请求服务器,直接使用本地缓存。
  • 11秒时,浏览器再次请求 xxx.js,请求服务器,带上上次修改时间 If-Modified-Since 和上次的 Etag 值 If-None-Match。
  • 服务器收到浏览器的 If-Modified-Since 和 Etag,发现有 If-None-Match,则比较 If-None-Match 和 xxx.js 的 Etag 值,忽略 If-Modified-Since 的比较。
  • xxx.js 文件内容没变化,则 Etag 和 If-None-Match 一致,服务器告诉浏览器继续使用本地缓存(304)。
  • 如此往复。

因为 Etag 的特性,所以相较于 Last-Modified 有一些优势:

  • 某些情况下服务器无法获取资源的最后修改时间
  • 资源的最后修改时间变了但是内容没变,使用 Etag 可以正确缓存
  • 如果资源修改非常频繁,在秒以下的时间进行修改,Last-Modified 只能精确到秒

实际场景下的缓存方案

我们不让 HTML 文件缓存,仅仅缓存静态资源文件(如 CSS、JS、图片等),每次访问 HTML 都去请求服务器。所以浏览器每次都能拿到最新的 HTML 资源。所以呢,我们在 HTML 上做些手脚。

PS. 之前实习的时候做一个需求,不小心让 HTML 加入了缓存,导致 bug 无法通过更新迭代的手段修复…所以 HTML 一定不能加进缓存!

方案一:加入版本号

  1. <script src="http://test.com/a.js?version=0.0.1"></script>

方案二:MD5 值控制(也是目前比较好的方案)

  1. <script src="http://test.com/a.hash值.js"></script>

参考资料