HTTP缓存


2020-09-07 上次更新时间:4/29/2022, 9:34:08 AM 0

# 介绍

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。它有以下几个优点:

  • 改善冗余数据传输(重复请求)
  • 缓解网络瓶颈(带宽限制)
  • 减少瞬间拥塞(多人同时访问)
  • 降低距离时延(距离造成的响应慢)

通过复用以前获取的资源,可以显著提高网站和应用程序的性能,让响应变得更快。缓存不是必须的,但是设置合适的缓存策略很有必要。

一个字总结:快!

# 命中与未命中

缓存主要是针对 GET 请求。对于到达缓存的请求,如果有副本可使用,则称为 “缓存命中” ,反之则称为 “缓存未命中”。

  • 缓存命中率:由缓存提供服务的请求所占的比例(对于中等web服务,40%就比较合理了)
  • 字节命中率:缓存提供的字节在传输的所有字节中所占的比例

实际上,HTTP 本身没有提供手段来区分响应式缓存命中的、但客户端提供了。 客户端通过 Date 和 Age 首部来检测该响应是否是缓存。在谷歌上的表现是 from disk cache 或 from memory cache。

# 缓存的拓扑结构

缓存可以分为:

  • 私有缓存 (浏览器):私有缓存只能用于单独用户,如浏览器缓存。
  • 公有代理缓存 (代理服务器):共享缓存可以被多个用户使用,如代理缓存服务器。

根据不同的缓存需求,缓存可以做层次结构,也可以做网状结构的代理,网状结构会更加复杂。

由于每个拦截代理都会添加一些性能损耗,所以代理层级不适合太深,一般两到三个代理比较适合。

# 缓存处理步骤

web 缓存对于一条 HTTP GET报文的基本缓存处理过程包括7个步骤:

  1. 接收 —— 从网络中读取抵达的请求报文
  2. 解析 —— 对报文进行解析,提取出 URL 和各种首部
  3. 查询 — —查看本地是否有副本可用,没有则获取一份并保存在本地
  4. 新鲜度检测 —— 查看已缓存副本是否足够新鲜,如果不是,则询问服务器是否有更新
  5. 创建响应 —— 用新的首部和已缓存的主体来构建响应报文
  6. 发送 —— 通过网络将响应发回给客户端
  7. 日志 —— 日志记录(可选)

# 缓存控制

与缓存相关的规则信息,均包含在 header 中。无论是 web 脚本文件、浏览器、服务器都可以设置这些 header,使用合适的缓存策略。

  • Cache-Control
  • expires
  • IF-Modified-Since:<date> + Last-Modified
  • If-None-Match:ETag + ETag

expires 是属于 HTTP/1.0 的,使用 expires 设置固定过期时间的缓存策略已经不适用现在的缓存需求,加上大部分浏览器默认的协议是 HTTP/1.1,所以默认使用的都是 Cache-Control。

# 优先级

当同时存在各种缓存头的时候,会遵循以下优先级:

  • 强缓存 > 对比缓存
  • cache-control > expires
  • ETag > Last-Modified
  • Pragma > cache-control

Pragma 属于 HTTP1.0 内容。在 HTTP/1.1 中,为了兼容,定义了唯一的使用方式 pragma: no-cache ,作用跟 Cache-Control:no-cache 一样。如果所有的中间服务器都是 HTTP/1.1 则只要设置 Cache-Control:no-cache 即可。

# 新鲜度检测

如果本地已有副本,缓存都需要进行新鲜度检测。新鲜度检测主要有两个机制:

  • 文档过期(document expiration)
  • 服务器再验证(server revalidation)

通过这两个检测判断是从缓存读取数据,还是从服务器读取数据。

# 1.文档过期检测

通过 Cache-Controlexpires 首部设置过期时间,如果没有其他阻止提供已缓存或未认证资源的首部时,缓存可以任意使用这些副本。

# Cache-Control

Cache-Control 是通用消息头字段,被用于 http 请求和响应中,通过指定指令来实现缓存机制。缓存指令是单向的,这意味着在请求中设置的指令,不一定被包含在响应中.

关于 Cache-Control 的详细介绍,请看 《MDN - Cache-Control》

常用 Cache-Control

private、public、no-cache、max-age,no-store,默认是 private。

  • 禁止缓存
Cache-Control: no-store
1
  • 缓存静态资源
Cache-Control:public, max-age=31536000
1
  • 需要重新验证
# 不使用缓存,强制向服务器重新请求数据
Cache-Control: no-cache
# 向服务器发起再验证请求,检测文档的新鲜度
Cache-Control: max-age=0
1
2
3
4

# 2.服务器再验证

验证是客户端和服务器的标识进行匹配的过程,也就是说,客户端的请求头会携带一个标识,服务器的响应头会携带一个标识,它们是对应的。

每次的再验证都可能出现以下的结果:

  • 内容发生变化(缓存未命中) =》获取新内容,返回完整的首部和实体数据 =》 状态码:200
  • 内容未变化(缓存命中) =》仅返回新的首部和过期日期 =》 状态码:304
  • 内容被删除 =》状态码:404 =》 缓存删除副本

成功的再验证是比缓存未命中更快的,因为再验证成功只需要返回一条 304 的响应和新的首部,并不会发送实体数据。

# Date日期再验证

  • 请求头:IF-Modified-Since:<date>(也称为 IMS 请求)
  • 响应头:Last-Modified:<date>

# ETag实体标签再验证

  • 请求头:If-None-Match:ETag
  • 响应头:ETag:ETag

强弱校验

使用 ETag 方法验证时,还可以区分为"强校验"和"弱校验"

  • 强检验:只要内容发生了改变,强验证器就会变化
etag: "5de485b2-ff14"
1
  • 弱校验(弱Etag):文件主体修改才验证,如注释的调整等则不会,标识是前缀会有 W/
etag: W/"b76dfd8b2c82dda6be9e3541d5e8df84"
1

# 优先级

从上面的图中可以看出 Last-Modified 和 ETag 经常会同时出现。如果同时使用了,它们的优先级如下:

ETag > Last-Modified

# 小结

总结下就是,在文档过期时间内,都会读取缓存数据,这种也被称为 "强缓存"。如果文档已过期或者有其他要求再验证时(如 Cache-Control: no-cache),就进入了服务器再验证环节,这时就称为 "对比缓存"

# 缓存数据的存储

缓存的拓扑结构可以知道,缓存是可以存在浏览器,也可以存在代理服务器上的。

# 浏览器缓存

浏览器将资源分为主资源和派生资源,如 HTML 页面就是主资源,HTML中的图片或者脚本连接,则称为派生资源。当允许浏览器缓存时,派生资源可以被存在磁盘中,也可以存储在内存中。

  • 磁盘缓存: 当浏览器关闭后,数据依然存在。Status Code: 200 (from memory cache)
  • 内存缓存:浏览器关闭后,数据消失。Status Code: 200 (from disk cache)

用一个简单的 html 页面作为示范:index.html

index.html

<!DOCTYPE html>
<html>
    <head>
        <title>test</title>
        <meta http-equiv="Cache-Control" content="max-age=60000" />
        <script src="b.js"></script>
        <link rel="stylesheet" type="text/css" href="c.css">
    </head>
    <body>
        index.html
        <div>
            <img src="./img/634&312.png">
        </div>
    </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. 首次访问:向服务器请求数据,正常返回数据并缓存,此时所有文件都是正常的 200 返回。
  1. 第二次访问(仅刷新):不同的文件的表现有差异
  • index.html - 304 Not Modified
  • b.js - 200 OK (from memory cache)
  • c.css - 200 OK (from disk cache)
  • test.png - 200 OK (from memory cache)
  1. 关闭浏览器后再次访问

可以看到所有的文件都从磁盘中读取了

# 浏览器缓存数据读取优先级

三级缓存原理

  1. 先去内存看,如果有,直接加载

  2. 如果内存没有,择取硬盘获取,如果有直接加载

  3. 如果硬盘也没有,那么就进行网络请求

  4. 加载到的资源缓存到硬盘和内存

# 小结

一般图片和很少改动的文件会用 disk cache, js文件通常是 memory cache。

# 代理服务器存储

nginx 可以通过配置 proxy_cache 进行缓存,具体配置请看 nginx

# nginx缓存控制

前端项目一般都使用 nginx 作为资源服务器,nginx 也可以作为一个代理缓存的服务器。

这几个例子是简单演示,更多的配置请看 nginx 官方文档: nginx

  • nginx 默认开启 ETag 和 Last-Modified。demo1
location /demo {
    alias /home/static/demo/;
    index index.html;
}
1
2
3
4
  • 使用 Cache-Control 控制缓存。demo2
location /store {
    alias /home/static/store/;
    index index.html;

    add_header Cache-Control public,max-age=3,600;
    expires 1h;
}
1
2
3
4
5
6
7
  • 关闭所有缓存,每次访问都是正常的 200 请求。demo3
location /nostore {
    alias /home/static/nostore/;
    index index.html;

    # 关闭 ETag 验证
    etag off;

    # 关闭 Last-Modified 验证
    if_modified_since off;
}
1
2
3
4
5
6
7
8
9
10

# 常用配置

location /demo {
    root /home/demo/;
    index index.html;
    client_max_body_size  100m;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  REMOTE-HOST $remote_addr;
    proxy_set_header  Host $host:$server_port;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    # add_header Cache-Control max-age=0,no-cache;
    if ( !-f $request_filename ) {
        rewrite ^/demo/([a-zA-Z]+)/(.*) /demo/$1/index.html last;
        add_header 'Cache-Control' 'max-age=0,no-cache';
        expires off;
        break;
    }
    if ($request_filename ~* "\.html$") {
        add_header Cache-Control max-age=0,no-cache;
    }
    if ($request_filename ~* "\.(js|css|png|jpe?g|gif|webp)$") {
        expires 72h;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 浏览器刷新操作

在浏览器中,通过不同的方式刷新页面会有不同的结果。

# 普通刷新操作:正常刷新,使用缓存数据。

  • Enter 回车刷新 —— 此时 Expires 生效时,则读取缓存
  • F5 / ctrl + R —— 此时 Expires 无效,Last-Modified/ETag 生效

# 强制刷新操作:强制刷新并重新加载数据,此时请求头默认带有 Cache-Control: no-cache

  • 勾选了 disable cache
  • ctrl + shift + R
  • ctrl + F5

浏览器刷新按钮可以选择是普通刷新还是强制刷新,默认是普通刷新。

参考文章:

上次更新时间: 4/29/2022, 9:34:08 AM