写在前面:最近学习了修言同学的小册,受益良多。对于HTTP缓存这一块,经过资料查询和思考,也有了自己的一些思考认识,希望分享出来与大家一起讨论和成长。
内容概述
- 什么是缓存及缓存的优点
- 缓存的处理步骤
- 强缓存和协商缓存
- 缓存决策
- 总结与思考
一、缓存及其优点
缓存
缓存是一种可以自动保存常见资源副本并可以在下一次请求中直接使用副本而非再次获取的技术。
也就是说,当我们首次进行资源请求之后,服务器在返回资源给客户端的同时,缓存服务器或本地缓存也会保存一份资源副本(在允许缓存的情况下),当我们下次再对该资源进行请求时,则会直接使用资源副本而不会从原始服务器再次请求文档。
缓存的优点
- 缓存可以减少冗余的数据传输。
- 缓存可以缓解网络瓶颈的问题。
- 缓存可以降低对原始服务器的要求。
- 缓存可以降低请求的距离时延。
数据的冗余传输
当很多客户端访问同一份文档的时候,原始服务器一遍又一遍地返回给不同的客户端相同内容的文档,这些重复的文档造成了数据的冗余传输。
网络瓶颈问题
在大部分情况下,客户端访问代理服务器的速度总是比访问原始服务器更快(带宽大、延迟低),因此如果代理服务器能够提供一份完整的副本,则远远比从原始服务器获取来的快且省流量——尤其针对大文件来说。
降低原始服务器要求
突发事件(比如爆炸性新闻、某个名人事件)使很多人几乎同时去访问一个Web文档时, 就会出现瞬间拥塞。由此造成的过多流量峰值可能会使网络和Web服务器产生崩溃。使用缓存便可在一定程度上降低对原服务器的压力。
降低请求距离时延
在物理上的距离,也是降低web性能的一个方面。对于同一份资源,原服务器离请求端越近,资源的获取速度则会越快。
二、强缓存和协商缓存
1、 缓存相关概念解释
缓存命中
如果某个请求的结果是由已缓存的副本提供的,被称作缓存命中。
缓存未命中
如果缓存中没有可用的副本或者副本已经过期,则会将请求转发至原始服务器,这被称作缓存未命中 。
新鲜度检测
HTTP通过缓存将服务器文档的副本保留一段时间。在这段时间里, 都认为文档是“新鲜的”,缓存可以在不联系服务器的情况下,直接提供该文档。但一旦已缓存副本停留的时间太长,超过了文档的新鲜度限值(freshness limit), 就认为对象“过时”了,在提供该文档之前,缓存要再次与服务器进行确认,以查看文档是否发生了变化。
再验证
原始服务器上的内容可能会随时变化,缓存需要经常对其进行检测,看看它保存的副本是否仍是服务器上最新的副本。这些新鲜度检测被称为 HTTP 再验证。
缓存可以随时对副本进行再验证,但大部分缓存只在客户端发起请求,并且副本旧得足以需要检测的时候,才会对副本进行再验证。
再验证命中和再验证未命中
缓存对缓存的副本进行再验证时,会向原始服务器发送一个再验证请求,如果内容没有发生变化,服务器会以304 Not Modified进行响应。这被称作是再验证命中或者缓慢命中。如果内容发生了变化,服务器会以200进行响应。这被称作再验证未命中。
2、 缓存的处理步骤
- 首先是当用户请求资源时,会判断是否有缓存,如果没有,则会向原服务器请求资源。
- 如果有缓存,则会进入强缓存的范畴,判断缓存是否新鲜,如果缓存新鲜,则会直接返回缓存副本给客户端。如果缓存不新鲜了,则表示强缓存失败,将会进入到协商缓存。
- 协商缓存将判断是否存在Etag和Last-Modified首部,通过这些首部验证资源是否发生过变化,如果未发生变化,则表示命中了协商缓存,会重定向到缓存副本,将资源返回给客户端,否则的话表示协商缓存未命中,服务器会返回新的资源。
大家可以先看几遍这张图,在脑海里对缓存的过程有一个宏观的了解,接下来我会对这张图上的各个部分进行解读。等到最后,再回来看这张图便会觉得缓存是如此简单的一个过程了。
3、 强缓存和协商缓存的概念
强缓存
服务端告知客户端缓存时间后,由客户端判断并决定是否使用缓存。
即首次发起请求时,服务端会在Response Headers 中写入缓存新鲜时间。当请求再次发出时,如果缓存新鲜,将直接从缓存获取资源,而不会再与服务器发生通信。
协商缓存
由服务端决定并告知客户端是否使用缓存。
协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。
4、 强缓存和协商缓存的实现原理
(1) 强缓存实现原理
强缓存是通过Expires首部或Cache-Control: max-age来实现的。
Expires 和 Cache-Control: max-age都是用来标识资源的过期时间的首部。
Expires(HTTP/1.0)
Expires描述的是一个绝对时间,由服务器返回,用GMT格式的字符串表示。
由于expires是一个绝对时间,如果人为的更改时间,会对缓存的有效期造成影响,使缓存有效期的设置失去意义。因此在http1.1中我们有了expires的完全替代首部cache-control:max-age
Cache-Control(HTTP/1.1)
max-age值是一个相对时间,它定义了文档的最大使用期——从第一次生成文档到文档不再新鲜、无法使用为止,最大的合法生存时间(以秒为单位)。
过程说明
- 当我们首次请求资源时,服务器在返回资源的同时,会在Response Headers中写入expires首部或cache-control,标识缓存的过期时间,缓存副本会将该部分信息保存起来。
- 当再次请求该资源的时候,缓存会对date(Date 是一个通用首部,表示原始服务器消息发出的时间。即表示的是首次请求某个资源的时间。)和expires/cache-control的时间进行对比,从而判断缓存副本是否足够新鲜。
(2) 协商缓存实现原理
协商缓存是通过请求头Last-Modified或Etag来实现的。
Last-Modified 标识的是文档最后修改时间,Etag 则是以文档内容来进行编码的。
Last-Modified
说明:
- 首次请求资源时,服务器在返回资源的同时,会在Response Headers中写入Last-Modified首部,表示该资源在服务器上的最后修改时间。
- 当再次请求该资源时,会在Request Headers 中写入If-Modified-Since首部,此时的If-Modified-Since的值是首次请求资源时所返回的Last-Modified的值。
- 服务器接收到请求后,会根据If-Modified-Since的值判断资源在该日期之后是否发生过变化。
- 如果没有,则会返回304 Not Modified;如果变化了,则会返回变化过后的资源,同时更新Last-Modified的值。
(1)资源未更新network面板截图
首次请求:
再次请求:
(2)资源发生更新network面板截图
首次请求:
再次请求:(大家可以看到Last-Modified和If-Modified-Since标识的时间不一样了)
Etag
我们可以看到,Etag的实现过程和Last-Modified完全一样,具体过程可参照Last-Modified,在这里就不做过多介绍了。
Last-Modified存在的一些问题
有些文档可能会被周期性地重写,但实际包含的数据常常是一样的。尽管内容没有变化,但修改日期会发生变化。
有些文档可能被修改了,但所做修改并不重要,不需要让缓存重载数据(比如对拼写或注释的修改)。
有些服务器提供的文档会在亚秒间隙发生变化(比如,实时监视器),对这些服务器来说,以一秒为粒度的修改日期可能就不够用了。
通过这些描述,我们可以总结出一些Last-Modified存在的缺陷:
- 无法感知文件内容是否真的发生了变化。 不该重新请求的时候,也会重新请求。
- 在秒以下的内容变化无法感知到。 该重新请求的时候,反而没有重新请求。
对于上述问题,Etag作为Last-Modified的补充而出现,Etag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,因此 Etag 能够精准地感知文件的变化。
Etag 强验证器和弱验证器
ETag 分为强验证器和弱验证器。
强验证器要求文档的每个字节都相等,而弱验证器只要求文档的含义相等。
强验证:
弱验证(前面会加上‘ W/’ 来标识):
5、 Cache-Control请求头常用属性说明
max-age/s-maxage
s-maxage指令的功能和max-age是相同的,它们唯一的不同点就在于s-maxage指令只适用于代理服务器缓存。s-maxage的优先级高于max-age。
public/private
public 与 private 是针对资源是否能够被代理服务缓存而存在的一组对立概念。
如果我们为资源设置了 public,那么它既可以被浏览器缓存,也可以被代理服务器缓存;如果我们设置了 private,则该资源只能被浏览器缓存。
no-cache/no-store
no-cache 表示客户端要求缓存在提供其已缓存的副本之前必须先和原始服务器对该文档进行验证。即强制跳过强缓存阶段,直接进行协商缓存。强缓存并不能知道缓存是否真的足够新鲜,使用no-cache就是为了防止从缓存中返回过期的资源,对缓存进行再验证。
no-store表示的是禁止缓存,即每一次都是直接与原服务器进行通信,从原服务器返回资源。一般设置了no-store的资源,都暗示着该资源具有敏感性信息。
6、优先级问题
(1)Expires 和 Cache-Control: max-age
应用HTTP/1.1版本的缓存服务器遇到同时存在Expires首部字段的情况时,会优先处理max-age指令,而忽略掉Expires首部字段。 而HTTP/1.0版本的缓存服务器的情况则相反,max-age指令会被忽略掉。
(2)Last-Modified 和 Etag(存疑部分)
看了很多资料都说Etag的优先级高于Last-Modified,但是又有资料说当Etag和Last-Modified同时存在时,是由二者共同决定标识文档是否发生变化的。
因此我对这里的优先级做了这样一番解读:当二者同时存在时,浏览器会优先判断Etag,如果If-None-Match和服务器资源最后修改时间不一样,则表示文件发生过变化,则直接返回200,此时不需要再对If-Modified-Since做检查。当Etag命中时,才会判断Last-Modified是否也命中,只有当二者都命中的情况下,才从缓存中获取缓存副本。
注意:此番观点不知是否正确,但觉得这样合乎情理,欢迎大家一起讨论或是指点一二。
三、缓存决策
由于并未有过http缓存方面的实际应用经验,在缓存决策方面实在没有什么自己的见解。
四、思考和总结
学习关于HTTP缓存方面的东西花费了不少时间,发现里面的概念和知识点比较的杂乱,一开始无法将整个缓存的过程串联起来。通过对博客和书籍的查阅,终于梳理清楚了缓存的流程及各个步骤中涉及到的概念和知识点。既对整个流程有了清晰的把控,也能对流程的各个环节和细节有一定的了解。
思考了很久应该如何行文,才能够既把大的流程讲述清楚,又能够兼顾每一步涉及到的东西。 希望通过分享,能够给大家带来一些新的东西和思考,那我也就满足了。
最后回忆一遍缓存的过程,在脑海里画出这张图:
最后感谢大家的阅读,辛苦啦^_^
如有错误,欢迎指正 weginjun@163.com
资料参考
- 掘金小册——前端性能优化原理与实践
- 浅谈浏览器http的缓存机制
- HTTP权威指南 (提取码:4e45)
- 图解HTTP