Skip to content

分类

  • 服务器缓存(代理服务器缓存、CDN 缓存)

  • 数据库缓存

  • 浏览器缓存

作用

  • 减少网络带宽消耗

  • 降低服务器压力

  • 减少网络延迟,加快页面打开速度。

浏览器缓存

从浏览器发起请求时,根据不同的缓存策略,去不同的位置获取相应的资源。

强制缓存

缓存期间内,浏览器不会向服务器发送任何请求,直接从本地缓存中读取文件并返回 200 OK。

先访问 memory cache,再访问 disk cache。

  • Expires(HTTP1.0)

    Expires: Thu, 06 Oct 2022 06:07:43 GMT

    值是一个 GMT 时间格式的绝对时间,告诉客户端资源在多长时间内是走缓存的,缓存期间, 直接从本地缓存中读取资源并返回 200 OK。

  • Cache-Control(HTTP1.1)

    Cache-Control: max-age=300

    表示从请求返回后的 300s 内都走缓存,是相对时间,弥补了 Expires 的缺陷,能够更全面 的控制内容,缓存期间,直接从本地缓存中读取资源并返回 200 OK。

    标记类型功能
    public响应头响应的数据,客户端和代理层都可以缓存。
    private响应头客户端可以缓存,代理层不能缓存(CDN,proxy_pass)
    no-cache请求头可以使用本地缓存,但是必须发送请求到服务器进行回源验证。
    no-store请求和响应禁用缓存
    max-age请求和响应以秒为单位进行缓存
    s-maxage请求和响应代理层缓存,CDN下发,当客户端数据过期时会重新校验。
    max-stale请求和响应缓存最大使用时间,如果缓存过期,但还在这个时间范围内则可以使用缓存数据。
    min-fresh请求和响应缓存最小使用时间
    must-revalidate请求和响应当缓存过期后,必须回源重新请求资源。比 no-cache 更严格。因为 HTTP 规范是允许 户端在某些特殊情况下直接使用过期缓存的,比如校验请求发送失败的时候。带有 must-revalidate 的缓存必须进行校验。
    proxy-revalidate请求和响应和 must-revalidate 类似,只对 CDN 这种代理服务器有效,客户端遇到此头,需要回 验证。
    stale-while-revalidate响应表示在指定时间内可以先使用本地缓存,后台进行异步校验。
    stale-if-error响应在指定时间内,重新验证时返回状态码为 5XX 的时候,可以用本地缓存。
    only-if-cached响应那么只使用缓存内容,如果没有缓存,返回 504 getway timeout。

Expires 和 Cache-Control 的区别

  • 时间:Expires 是 http1.0 的产物,Cache-Control 是 http1.1 的产物。

  • 精度:Expires 是一个绝对时间,如果客户端修改时间,缓存命中与否就不是开发者所期望的, Cache-Control 是一个相对时间,容易控制。

  • 优先级:Cache-Control 优先级高于 Expires, 在 HTTP1.0 下,Expires 才会发挥作用。 现阶段它的存在只是一种兼容性的写法,nginx 和 node 中 HTTP 版本默认都是 1.1,nginx 中如果同时设置 expires 和 cache-control,cache-control 会失效,node 中设置 expires 响应头会报错。

协商缓存

浏览器请求头中携带缓存标识 If-Modifield-Since、If-None-Match 向服务器发请求,服务器 根据缓存标识判断是否命中协商缓存,如果命中返回 304 继续使用缓存,否则返回 200 OK 和最新 资源。

  • Last-Modified(HTTP1.0)

    通过对比资源的修改时间来判断资源是否更新。

  • Etag(HTTP1.1)

    通过对资源进行哈希运算来判断资源是否更新。

Last-Modified 与 Etag 区别:

  • 时间:Last-Modifield 是HTTP1.0 的产物,Etag 是 HTTP1.1 的产物。

  • 精度:ETag 要优于 Last-Modifield,Last-Modified 策略的时间单位为秒,意味着在秒级的 请求上,Last-Modified 精度不够用,并且在使用负载均衡(集群)的服务器上,各个服务器中 资源的的Last-Modified 也有可能不相同,ETag 只要资源内容改变值就会变化从而确保精度。

  • 优先级: ETag > Last-Modifield

  • 性能:ETag 要差于 Last-Modified,Last-Modified 策略只是记录时间,而 ETag 需要进行 摘要运算。

为什么有了 Last-Modified 还要有 Etag?

  • Last-Modified 只能精确到秒级,如果某些文件在 1s 内修改多次,不一定能获取到最新的 资源, 而 ETag 精度更高, 能获取到最新的资源。

  • 某些文件会被定期生成,而内容并有变化,但 Last-Modified 却更新了, 没有达到使用缓存的 目的。

  • 可能存在服务器不能准确获取文件修改时间或者与代理服务器时间不一致等情形。

流程

第一次请求时,响应头中返回 Cache-Control、Last-Modified、Etag 字段,浏览器会将它们的 值保存起来。

第二次请求时,浏览器先访问 memory cache,再访问 disk cache,如果能找到缓存且没有过期 则使用缓存并返回 200 OK,如果过期则向服务器发送请求,请求头中添加 If-Modifield-Since 和 If-None-Match 字段,值就是第一次请求时响应头中 Last-Modified、Etag 字段的值,如果 资源没有更新则返回 304 继续使用缓存并在响应头中更新 Cache-Control、Last-Modified、 Etag 字段,资源更新了则返回 200 OK 和最新资源并在响应头中更新 Cache-Control、 Last-Modified、Etag 字段。

强制缓存的缺陷

仅仅关心缓存是否过期,并不关心这个时间段内资源是否更新,会导致缓存期间内客户端拿不到 最新的静态资源。

  • query:资源统一加版本号,缺点是只想更新 foo.css 会导致 bar.css 缓存也失效。

    html
    <!-- index.html v=001 -->
    <link rel="stylesheet" href="foo.css?v=001" />
    <link rel="stylesheet" href="bar.css?v=001" />
    <div class="foo">foo</div>
    
    <!-- index.html v=002 -->
    <link rel="stylesheet" href="foo.css?v=002" />
    <link rel="stylesheet" href="bar.css?v=002" />
    <div class="foo">foo</div>
  • query-hash,将资源内容与版本号绑定,资源发生变化时才更新版本号,通过消息摘要算法, 对文件求摘要信息,这样就可以精确到单文件粒度的缓存控制。

    html
    <!-- index.html v=001 -->
    <link rel="stylesheet" href="foo.css?v=dw5i316cw" />
    <link rel="stylesheet" href="bar.css?v=kf4516lod" />
    <div class="foo">foo</div>
    
    <!-- index.html v=002 -->
    <link rel="stylesheet" href="foo.css?v=ac4a562gw" />
    <link rel="stylesheet" href="bar.css?v=kf4516lod" />
    <div class="foo">foo</div>

覆盖式发布引发的问题

某次更新时,更改了 foo.css 样式,此时会将 HTML 中的 foo.css 的 url 更新为最新的 hash, 并将服务器中存储的 foo.css & index.html 文件覆盖为最新(V2版本),看似 HTML 和静态资 源都对应更新了,但是没有考虑极端情况。那就是:

先部署静态资源,部署期间访问时,会出现 V1 版本 HTML 访问到 V2 版本新静态资源,并按 V1-hash 缓存起来。

先部署 HTML,部署期间访问时,会出现 V2 版本 HTML 访问到 V1 版本旧静态资源,并按 V2-hash 缓存起来。

问题的根源是静态资源只有一份,每次都是覆盖式发布,导致了旧页面匹配到新资源或者新页面匹配 到旧资源的错误,通过非覆盖式发布解决,可以将文件摘要放到 url 中,即 query-hash 改为 name-hash。

html
<!-- index.html v=001 -->
<link rel="stylesheet" href="foo.dw5i316cw.css" />
<link rel="stylesheet" href="bar.kf4516lod.css" />
<div class="foo">foo</div>

<!-- index.html v=002 -->
<link rel="stylesheet" href="foo.ac4a562gw.css" />
<link rel="stylesheet" href="bar.kf4516lod.css" />
<div class="foo">foo</div>

这样,每次部署时都是先全量部署静态资源,再灰度部署页面,解决了缓存的问题,此时服务器会存 在多份 foo.[$hash].css 文件。

用户行为对缓存策略的影响

  • F5 刷新,浏览器会在请求头中设置 Cache-Control: max-age=0,跳过强制缓存,进行 协商缓存。

  • Ctrl + F5 刷新,跳过强制缓存和协商缓存,直接从服务器获取资源。