缓存
# 缓存简介
缓存控制:控制缓存的开关,用于标识请求或访问中是否开启了缓存,使用了哪种缓存方式。
缓存校验:如何校验缓存,比如怎么定义缓存的有效期,怎么确保缓存是最新的。
缓存命中率:从缓存中得到数据的请求数与所有请求数的比率。理想状态是越高越好。
控制缓存开关的字段有两个:Pragma 和 Cache-Control。
- 禁止浏览器从本地计算机的缓存中访问页面内容
<meta http-equiv="Pragma" content="no-cache">
# 缓存作用
减少网络带宽消耗:无论对于网站运营者或者用户,带宽都代表着金钱,过多的带宽消耗,只会便宜了网络运营商。当 Web 缓存副本被使用时,只会产生极小的网络流量,可以有效的降低运营成本。
降低服务器压力:给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,间接降低服务器的压力。同时,搜索引擎的爬虫机器人也能根据过期机制降低爬取的频率,也能有效降低服务器的压力。
减少网络延迟,加快页面打开速度:带宽对于个人网站运营者来说是十分重要,而对于大型的互联网公司来说,可能有时因为钱多而真的不在乎。那 Web 缓存还有作用吗?答案是肯定的,对于最终用户,缓存的使用能够明显加快页面打开速度,达到更好的体验。
# 缓存介质
虽然从硬件介质上来看,无非就是内存和硬盘两种,但从技术上,可以分成内存、硬盘文件、数据库。
内存:将缓存存储于内存中是最快的选择,无需额外的 I/O 开销,但是内存的缺点是没有持久化落地物理磁盘,一旦应用异常 break down 而重新启动,数据很难或者无法复原。
硬盘:一般来说,很多缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的情况下,可以被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的。
数据库:前面有提到,增加缓存的策略的目的之一就是为了减少数据库的 I/O 压力。现在使用数据库做缓存介质是不是又回到了老问题上了?其实,数据库也有很多种类型,像那些不支持 SQL,只是简单的 key-value 存储结构的特殊数据库(如 BerkeleyDB 和 Redis),响应速度和吞吐量都远远高于我们常用的关系型数据库等。
# 前端缓存
前端缓存主要可以分为 HTTP 缓存和浏览器缓存。
# 缓存流程图
# HTTP 缓存
# http 缓存分类
# http 缓存过程
浏览器第一次发起请求步骤:
在第一次请求时,服务器会将页面最后修改时间通过 Last-Modified 标识由服务器发送给客户端,客户端记录修改时间;服务器还会生成一个 Etag,并发送给客户端。
浏览器后续再次进行请求时:
(1) 判断是否是强缓存:浏览器在请求某一资源时,会先获取该资源缓存的 header 信息,判断是否命中强缓存(cache-control 和 expires 信息),若命中直接从缓存中获取资源信息,包括缓存 header 信息;本次请求根本就不会与服务器进行通信;
(2) 判断是否是协商缓存:如果没有命中强缓存,浏览器会发送请求到服务器,请求会携带第一次请求返回的有关缓存的 header 字段信息(Last-Modified/If-Modified-Since 和 Etag/If-None-Match),由服务器根据请求中的相关 header 信息来比对结果是否协商缓存命中;若命中,则服务器返回新的响应 header 信息更新缓存中的对应 header 信息,但是并不返回资源内容,它会告知浏览器可以直接从缓存获取;否则返回最新的资源内容
# 强缓存
强缓存:强制从浏览器缓存中查找该次请求的标识和内容,然后根据该结果的缓存规则,来决定是否使用浏览器缓存。
控制强制缓存的字段是
Expires
和Cache-Control
Cache-Control 优先级比 Expires 高。
- Cache-Control
Cache-Control:请求/响应头,缓存控制字段,精确控制缓存策略。
在 HTTP/1.1 中,Cache-Control 是最重要的规则,主要用于控制网页缓存,主要取值为:
public:所有内容都将被缓存(客户端和代理服务器(如 CDN)都可缓存)
private:所有内容只有客户端可以缓存,Cache-Control 的默认取值
no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
max-age=xxx (xxx is numeric):缓存内容将在 xxx 秒后失效
- Expires
简介:响应头,代表该资源的过期时间。
是 http1.0 的规范,它的值是一个绝对时间的 GMT 格式的时间字符串
这个时间代表这这个资源的失效时间,只要发送请求时间是在 Expires 之前,那么本地缓存始终有效,则在缓存中读取数据。所以这种方式有一个明显的缺点,由于失效的时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱
# 协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程:
主要涉及到两组 header 字段:
Etag 和 If-None-Match
Last-Modified 和 If-Modified-Since
# Last-Modified/If-Modified-Since:
- Last-Modified
简介:响应头,资源最近修改时间,由服务器告诉浏览器。
- If-Modified-Since
简介:请求头,资源最近修改时间,由浏览器告诉服务器。
(1) 浏览器第一次发送请求,让服务端在 response header 中返回请求的资源上次更新时间(Last-Modified 的值),浏览器会存下这个时间;
(2) 当浏览器下次请求时,request header 中带上 If-Modified-Since(即保存的 Last-Modified 的值)。根据浏览器发送的修改时间和服务端的 wh 修改时间进行比对,一致的话代码资源没有改变,服务端返回正文为空的响应,让浏览器在缓存中读取资源,从而减少请求消耗。
缺点:
1、 Last-Modified 保存的是绝对时间,并且是精确到秒,所以如果资源在 1 秒内修改了多次的话,那就无法识别;
2、对于文件只改变了修改时间,内容不变,这时候也会使缓存失效,其实这个时候我们是不希望客户端重新请求的;
3、某些服务器不能精确的得到文件的最后修改时间;
# ETag/If-None-Match:
- ETag
简介:响应头,资源标识,由服务器告诉浏览器。
- If-None-Match
简介:请求头,缓存资源标识,由浏览器告诉服务器。
- 浏览器第一次发送一个请求得到 ETag 的值,然后在下一次请求 request header 中带上 If-none-match(即保存的 ETag 的值);
- 通过发送的 ETag 的值和服务器重新生成的 ETag 的值进行比对,如果一致代表资源没有改变,服务器返回的正文为空的响应,让浏览器从缓存中读取资源,从而减少请求消耗。
ETag 的工作机制跟 Last-Modified 基本一样。但是,ETag 是对资源内容使用抗碰撞散列函数(我也不知道是啥),使用最近修改的时间戳的哈希值。ETag 解决了 Last-Modified 上述问题。
缺点:
- ETag 虽然能解决问题,但也并非完美,ETag 每次服务端生成都需要进行读写操作(因为要生成 hash),而 Last-Modified 只需要读取操作,ETag 消耗更大一些。
Last-Modified 和 ETag 同样可以同时配置,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回 304。
精确度上,ETag 更高,性能上 Etag 稍微低一点(因为要生成 hash)
由此可见,协商缓存其实受强缓存的影响,强缓存过期了且 Cache-Control 不为 no-store 时是否缓存才由协商缓存决定。
# 为什么要有 etag
你可能会觉得使用 Last-Modified 已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要 Etag 呢?HTTP1.1 中 Etag 的出现主要是为了解决几个 Last-Modified 比较难解决的问题:
一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新 GET;
某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说 1s 内修改了 N 次),If-Modified-Since 能检查到的粒度是 s 级的,这种修改无法判断(或者说 UNIX 记录 MTIME 只能精确到秒);
某些服务器不能精确的得到文件的最后修改时间。
这时,利用 Etag 能够更加准确的控制缓存,因为 Etag 是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符。
Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回 304。
# Last-Modified 生成原理
一般会选文件的 mtime,表示文件内容的修改时间
为什么使用 mtime 而非 ctime?
mtime 和 ctime 的区别在于,只有修改了文件的内容,才会更新文件的 mtime,而对文件更名,修改文件的属主等操作,只会更新 ctime。
# ETag 生成原理
nginx 中 ETag 的生成:nginx 中 etag 由响应头的 Last-Modified 与 Content-Length 表示为十六进制组合而成。
如果 http 响应头中 ETag 值改变了,是否意味着文件内容一定已经更改?
不能。
Last-Modified 是由一个 unix timestamp 表示,则意味着它只能作用于秒级的改变,而 nginx 中的 ETag 添加了文件大小的附加条件,因此使用 nginx 计算 304 有一定局限性:在 1s 内修改了文件并且保持文件大小不变。但这种情况出现的概率极低就是了,因此在正常情况下可以容忍一个不太完美但是高效的算法。
参考:https://github.com/shfshanyue/Daily-Question/issues/112 (opens new window)
# 强缓存和协商缓存的区别
- 是否请求服务器:强缓存不发请求到服务器,协商缓存会发请求到服务器。
- 状态码不一致:强缓存 200,协商缓存为 304
# 三种方式设置浏览器过期时间
设置响应头(注意浏览器有自己的缓存替换策略,即便资源过期,不一定被浏览器删除。同样资源未过期,可能由于缓存空间不足而被其他网页新的缓存资源所替换而被删除。):
1、设置 Cache-Control: max-age=1000 //响应头中的 Date 经过 1000s 过期
2、设置 Expires //此时间与本地时间(响应头中的 Date )对比,小于本地时间表示过期,由于本地时钟与服务器时钟无法保持一致,导致比较不精确
3、如果以上均未设置,却设置了 Last-Modified ,浏览器隐式的设置资源过期时间为 (Date - Last-Modified) * 10% 缓存过期时间。
# 用户的行为对缓存的影响
- 如何重新加载强缓存缓存过的资源
通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新资源。加 query 值
例如:<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js?v=1.0.0"></script>
# 浏览器缓存
# cookie
# CDN 缓存(服务端缓存)
CDN 缓存是一种服务端缓存
CDN 服务商将源站的资源缓存到遍布全国的高性能加速节点上,
当用户访问相应的业务资源时,用户会被调度至最接近的节点最近的节点 ip 返回给用户,
在 web 性能优化中,它主要起到了,缓解源站压力,优化不同用户的访问速度与体验的作用。
CDN 的优势:
- (1)CDN 节点解决了跨运营商和跨地域访问的问题,访问延时大大降低;
- (2)大部分请求在 CDN 边缘节点完成,CDN 起到了分流作用,减轻了源站的负载。
CDN 缺点:
- 当网站更新时,如果 CDN 节点上数据没有及时更新,即便用户再浏览器使用 Ctrl +F5 的方式使浏览器端的缓存失效,也会因为 CDN 边缘节点没有同步最新数据而导致用户访问异常。
# CDN 缓存策略
CDN 边缘节点缓存策略因服务商不同而不同,但一般都会遵循 http 标准协议,通过 http 响应头中的 Cache-control: max-age 的字段来设置 CDN 边缘节点数据缓存时间。
当客户端向 CDN 节点请求数据时,CDN 节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;否则,CDN 节点就会向源站发出回源请求,从源站拉取最新数据,更新本地缓存,并将最新数据返回给客户端。
CDN 服务商一般会提供基于文件后缀、目录多个维度来指定 CDN 缓存时间,为用户提供更精细化的缓存管理。
CDN 缓存时间会对“回源率”产生直接的影响。若 CDN 缓存时间较短,CDN 边缘节点上的数据会经常失效,导致频繁回源,增加了源站的负载,同时也增大的访问延时;若 CDN 缓存时间太长,会带来数据更新时间慢的问题。开发者需要增对特定的业务,来做特定的数据缓存时间管理。
# CDN 缓存刷新
CDN 边缘节点对开发者是透明的,相比于浏览器 Ctrl+F5 的强制刷新来使浏览器本地缓存失效,开发者可以通过 CDN 服务商提供的“刷新缓存”接口来达到清理 CDN 边缘节点缓存的目的。这样开发者在更新数据后,可以使用“刷新缓存”功能来强制 CDN 节点上的数据缓存过期,保证客户端在访问时,拉取到最新的数据。
# DNS 缓存
我们知道 url 其实只是一个别名,真实的服务器请求地址,实际上是一个 IP 地址。获得 IP 地址的方式,就是查询 DNS 映射表。虽然这是一个非常简单的查询,但如果每次用户访问一个 url 都去查询 DNS 一次,未免显得太频繁。DNS 会告诉你,你别老是经常过来,万一我挂了,我们就无法愉快地玩耍了。
DNS 查询过程大约消耗 20 毫秒,在 DNS 查询过程中,浏览器什么都不会做,保持空白。如果 DNS 查询很多,网页性能会受到很大影响,因此需要用到 DNS 缓存。 不同浏览器的缓存机制不同: IE 对 DNS 记录默认的缓存时间为 30 分钟,Firefox 对 DNS 记录默认的缓存时间为 1 分钟,Chrome 对 DNS 记录默认的缓存时间为 1 分钟。
缓存时间长:减少 DNS 的重复查找,节省时间。 缓存时间短:及时检测服务器的 IP 变化,保证访问的正确性。