HTTP缓存
# 介绍
缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。它有以下几个优点:
- 改善冗余数据传输(重复请求)
- 缓解网络瓶颈(带宽限制)
- 减少瞬间拥塞(多人同时访问)
- 降低距离时延(距离造成的响应慢)
通过复用以前获取的资源,可以显著提高网站和应用程序的性能,让响应变得更快。缓存不是必须的,但是设置合适的缓存策略很有必要。
一个字总结:快!
# 命中与未命中
缓存主要是针对 GET
请求。对于到达缓存的请求,如果有副本可使用,则称为 “缓存命中” ,反之则称为 “缓存未命中”。
- 缓存命中率:由缓存提供服务的请求所占的比例(对于中等web服务,40%就比较合理了)
- 字节命中率:缓存提供的字节在传输的所有字节中所占的比例
实际上,HTTP 本身没有提供手段来区分响应式缓存命中的、但客户端提供了。 客户端通过 Date 和 Age 首部来检测该响应是否是缓存。在谷歌上的表现是 from disk cache 或 from memory cache。
# 缓存的拓扑结构
缓存可以分为:
- 私有缓存 (浏览器):私有缓存只能用于单独用户,如浏览器缓存。
- 公有代理缓存 (代理服务器):共享缓存可以被多个用户使用,如代理缓存服务器。
根据不同的缓存需求,缓存可以做层次结构,也可以做网状结构的代理,网状结构会更加复杂。
由于每个拦截代理都会添加一些性能损耗,所以代理层级不适合太深,一般两到三个代理比较适合。
# 缓存处理步骤
web 缓存对于一条 HTTP GET报文的基本缓存处理过程包括7个步骤:
- 接收 —— 从网络中读取抵达的请求报文
- 解析 —— 对报文进行解析,提取出 URL 和各种首部
- 查询 — —查看本地是否有副本可用,没有则获取一份并保存在本地
- 新鲜度检测 —— 查看已缓存副本是否足够新鲜,如果不是,则询问服务器是否有更新
- 创建响应 —— 用新的首部和已缓存的主体来构建响应报文
- 发送 —— 通过网络将响应发回给客户端
- 日志 —— 日志记录(可选)
# 缓存控制
与缓存相关的规则信息,均包含在 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-Control
和 expires
首部设置过期时间,如果没有其他阻止提供已缓存或未认证资源的首部时,缓存可以任意使用这些副本。
# 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
- 缓存静态资源
Cache-Control:public, max-age=31536000
- 需要重新验证
# 不使用缓存,强制向服务器重新请求数据
Cache-Control: no-cache
# 向服务器发起再验证请求,检测文档的新鲜度
Cache-Control: max-age=0
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"
- 弱校验(弱Etag):文件主体修改才验证,如注释的调整等则不会,标识是前缀会有
W/
etag: W/"b76dfd8b2c82dda6be9e3541d5e8df84"
# 优先级
从上面的图中可以看出 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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 首次访问:向服务器请求数据,正常返回数据并缓存,此时所有文件都是正常的 200 返回。
- 第二次访问(仅刷新):不同的文件的表现有差异
- 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)
- 关闭浏览器后再次访问
可以看到所有的文件都从磁盘中读取了
# 浏览器缓存数据读取优先级
三级缓存原理
先去内存看,如果有,直接加载
如果内存没有,择取硬盘获取,如果有直接加载
如果硬盘也没有,那么就进行网络请求
加载到的资源缓存到硬盘和内存
# 小结
一般图片和很少改动的文件会用 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;
}
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;
}
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;
}
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;
}
}
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
浏览器刷新按钮可以选择是普通刷新还是强制刷新,默认是普通刷新。
参考文章: