HTTP缓存在WEB系统性能优化的过程中,起到了不可忽视的作用。而我们又常常将它忽视😂,这次给它一个面子,好好了解一下缓存。
缓存的目标以及顺序
目标
常见的 HTTP 缓存只能存储 GET 响应,对于其他类型的响应则无能为力。缓存的关键主要包括request method和目标URI。
顺序
- 接收、解析—从网络中读取抵达的请求报文,对报文进行解析,提取出 URL 和各种首部。
- 查询—缓存查看是否有本地副本可用,如果没有,就获取一份副本(并将其保存在本地)。
- 新鲜度检测—缓存查看已缓存副本是否足够新鲜,如果不是,就询问服务器是否有任何更新。
- 创建响应—缓存会用新的首部和已缓存的主体来构建一条响应报文。
- 发送—缓存通过网络将响应发回给客户端。
- 日志—缓存可选地创建一个日志文件条目来描述这个事务。
为什么使用缓存?
1. 冗余的数据传输
多个客户端同时访问一个WEB服务页面,服务器会多次传输同一份文档,每次传送给一个客户端。一些相同的字节会在网络中一遍遍地传输。这些冗余的数据传输会耗尽昂贵的网络带宽,降低传输速度,加重 Web 服务器的负载。
2. 带宽瓶颈
很多网络为本地网络客户端提供的带宽比为远程服务器提供的带宽要宽,客户端会以路径上最慢的网速访问服务器。如果客户端从一个快速局域网的缓存中得到了一份副本,那么缓存就可以提高性能——尤其是要传输比较大的文件时。
3. 瞬间拥塞
缓存在破坏瞬间拥塞(Flash Crowds)时显得非常重要。'突发事件',使很多人几乎同时去访问一个 Web 文档时,就会出现瞬间拥塞。由此造成的过多流量峰值,可能会使网络和 Web 服务器产生灾难性的崩溃。
4. 距离时延
即使带宽不是问题,距离也可能成为问题。每台网络路由器都会增加因特网流量的时延。即使客户端和服务器之间没有太多的路由器,光速自身也会造成显著的时延。
常见的缓存头
缓存控制字段 | 说明 |
---|---|
Cache-Control 头 | HTTP/1.1定义的 Cache-Control 头用来区分对缓存机制的支持情况, 请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。 |
Pragma 头 | Pragma 是HTTP/1.0标准中定义的一个header属性,请求中包含Pragma的效果跟在头信息中定义Cache-Control: no-cache相同,但是HTTP的响应头不支持这个属性,所以它不能拿来完全替代HTTP/1.1中定义的Cache-Control头。通常定义Pragma以向后兼容基于HTTP/1.0的客户端。 |
Etag | http1.1时期新加属性 ,使用inode+mtime(以下有解释)来计算。根据实体内容生成的一段hash字符串(类似于MD5或者SHA1之后的结果),可以标识资源的状态。 当资源发送改变时,ETag也随之发生变化。 |
Last-Modified | http1.1时期属性,比较资源最后一次修改时间 |
缓存对比字段 | 说明 |
---|---|
Expires | http1.0时期属性,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。一般用来做兼容。 |
Vary | HTTP 响应头决定了对于后续的请求头,如何判断是请求一个新的资源还是使用缓存的文件。 |
If-Match | 比较ETag是否一致, 在请求方法为 GET 和 HEAD 的情况下,服务器仅在请求的资源满足此首部列出的 ETag 之一时才会返回资源。而对于 PUT 或其他非安全方法来说,只有在满足条件的情况下才可以将资源上传。 |
If-None-Match | 比较ETag是否不一致,对于 GETGET 和 HEAD 请求方法来说,当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端会才返回所请求的资源,响应码为 200 。对于其他方法来说,当且仅当最终确认没有已存在的资源的 ETag 属性值与这个首部中所列出的相匹配的时候,才会对请求进行相应的处理。 |
If-Modified-Since | 比较资源最后更新的时间是否一致,如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 响应,而在 Last-Modified 首部中会带有上次修改时间。 不同于 If-Unmodified-Since, If-Modified-Since 只可以用在 GET 或 HEAD 请求中。 |
If-Unmodified-Since | 比较资源最后更新的时间是否不一致,只有当资源在指定的时间之后没有进行过修改的情况下,服务器才会返回请求的资源,或是接受 POST 或其他 non-safe 方法的请求。如果所请求的资源在指定的时间之后发生了修改,那么会返回 412 (Precondition Failed) 错误。 |
If-Range | 头字段通常用于断点续传的下载过程中,用来自从上次中断后,确保下载的资源没有发生改变。 |
缓存的发展
HTTP1.0近时代
给客户端设定缓存可通过两个字段,Pragma
跟Expires
来实现。
Pragma
字段值为no-cache的时候,会告诉客户端不要对该资源读缓存,即每次都得向服务器发一次请求才行。Expires
的值对应一个GMT(格林尼治时间),比如Tue, 09 Oct 2018 10:22:09 GMT
GMT来告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求。-
响应报文中Expires所定义的缓存时间是相对服务器上的时间而言的,其定义的是资源过期时刻。
-
如果客户端上的时间跟服务器上的时间不一致(特别是用户修改了自己电脑的系统时间),那缓存时间可能就没啥意义了。
-
如果Pragma头部和Expires头部同时存在,则起作用的会是Pragma
HTTP1.1后时代
因为1.0时代的缓存问题,“Expires时间是相对服务器而言的,无法保证和客户端时间统一”,http1.1新增了 Cache-Control 来定义缓存过期时间。
- 若报文中同时出现了 Expires 和 Cache-Control,则以 Cache-Control 为准。
- 优先级从低到高
Expires <- Cache-Control <- Pragma
Cache-Control 字段简介
在RFC中规范了 Cache-Control 的格式为:"Cache-Control" ":" cache-directive
。
作用 | 字段 | 说明 |
---|---|---|
禁止进行缓存 | no-store | 缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。 |
强制确认缓存 | no-cache | 每次有请求发出时,缓存会将此请求发到服务器(该请求应该会带有与本地缓存相关的验证字段),服务器端会验证请求中所描述的缓存是否过期,若未过期(实际就是返回304),则缓存才使用本地缓存副本。 |
私有缓存 | private | 则表示该响应是专用于某单个用户的,中间人不能缓存此响应,该响应只能应用于浏览器私有缓存中。 |
公共缓存 | public | 指令表示该响应可以被任何中间人(比如中间代理、CDN等)缓存。若指定了"public",则一些通常不被中间人缓存的页面(因为默认是private)(比如 带有HTTP验证信息(帐号密码)的页面 或 某些特定影响状态码的页面),将会被其缓存。 |
缓存过期机制 | max-age=<seconds> |
表示资源能够被缓存(保持新鲜)的最大时间。相对Expires而言,max-age是距离请求发起的时间的秒数。针对应用中那些不会改变的文件,通常可以手动设置一定的时长以保证缓存有效,例如图片、css、js等静态资源。 |
缓存验证确认 | must-revalidate | 缓存在考虑使用一个陈旧的资源时,必须先验证它的状态,已过期的缓存将不被使用 |
客户端决定是否向服务器发送请求,比如设置的缓存时间未过期,那么自然直接从本地缓存取数据即可,若缓存时间过期了或资源不该直接走缓存,则会发请求到服务器去。
但是只有这样还不够,我们来假设一种情况。缓存过期了,服务器上的这个资源数据量够多,但又没更改过。那这个时候重新请求的话,相当于白白加载了一遍,浪费带宽跟时间。
Last-Modified 响应头
服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端。
这个响应头也被叫做弱类型校验器,说它弱是因为它只能精确到一秒。
Last-Modified: Thu, 30 Aug 2018 08:03:28 GMT
- 当向服务端发起缓存校验的请求时,会比较两个时间是否一致,服务端会返回 200 表示返回正常的结果或者 304 Not Modified(不返回body)表示浏览器可以使用本地缓存文件。
- 需要注意的是,304的响应头也可以同时更新缓存文档的过期时间。
跟其配合的常常有 If-Modified-Since、If-Unmodified-Since等(看上面缓存头解释)
ETag 响应头
- 作为缓存的一种强校验器,ETag 响应头是一个对用户代理(User Agent)不透明的值。
- 为了解决上述Last-Modified可能存在的不准确的问题,Http1.1还推出了 ETag 实体首部字段。 服务器会通过某种算法,给资源计算得出一个唯一标志符(比如md5标志),在把资源响应给客户端的时候,会在实体首部加上“ETag: 唯一标识符”一起返回给客户端。
Etag: "4280832337"
Etag: W/"57a1bb7b-10c8" // 表示使用弱验证器
-
客户端会保留该 ETag 字段,并在下一次请求时将其一并带过去给服务器。
-
服务器只需要比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。
-
如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端。
-
如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。
跟其配合的常常有 If-Match、If-None-Match等(看上面缓存头解释)
缓存头部效果对比
头部 | 优势和特点 | 劣势和问题 |
---|---|---|
Expires | 1. HTTP 1.0 产物,可以在HTTP 1.0和1.1中使用,简单易用。 2. 以时刻标识失效时间。 |
1. 时间是由服务器发送的(UTC),如果服务器时间和客户端时间存在不一致,可能会出现问题。 2. 存在版本问题,到期之前的修改客户端是不可知的。 |
Cache-Control | 1. HTTP 1.1 产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题。 2. 比Expires多了很多选项设置。 |
1. HTTP 1.1 才有的内容,不适用于HTTP 1.0 。 2. 存在版本问题,到期之前的修改客户端是不可知的。 |
Last-Modified | 不存在版本问题,每次请求都会去服务器进行校验。服务器对比最后修改时间如果相同则返回304,不同返回200以及资源内容。 | 1. 只要资源修改,无论内容是否发生实质性的变化,都会将该资源返回客户端。例如周期性重写,这种情况下该资源包含的数据实际上一样的。 2. 以时刻作为标识,无法识别一秒内进行多次修改的情况。 3. 某些服务器不能精确的得到文件的最后修改时间。 |
ETag | 1. 可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况。 2. 不存在版本问题,每次请求都回去服务器进行校验。 |
1. 计算ETag值需要性能损耗。 2. 分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时发现ETag不匹配的情况。 |
缓存实践
强缓存(200 from cache)
- 为静态资源配置一个超过当前时长的Expires或Cache-Control。
- 这样用户在访问网页时,只会在第一次加载时从服务器请求静态资源。
- 只要缓存没有失效并且用户没有强制刷新的条件下都会从自己的缓存中加载。
协商缓存(304 Not Modified)
当浏览器对某个资源的请求没有命中强缓存
- 服务器还可以配置 http response header 的 ETag (http1.1) 字段,表示当前资源的唯一标识。
- 如果资源在客户端过期,发送资源请求到服务器有 http request header 有 If-None-Match 字段,则表示当前资源配置过 ETag 字段。
- 此时服务器需要判断当前资源的唯一标识 ETag 值是否与请求的 If-None-Match 字段值一致,如果一致则表示当前资源未修改,可以继续使用缓存资源,这就是
304 not-modified (协商缓存)
。
缓存命中速度
缓存命中 > 缓存再验证成功 > 缓存未命中 = 缓存再验证失败;
200(from memory cache) 和 200(from disk cache)
在用chrome查看缓存的过程中,发现了一个问题,那就是有的缓存资源是
200(from memory cache)
,而有的资源是200(from disk cache)
,这一奇特的现象让我百思不得解呀!
先看看他们的解释。
-
MemoryCache顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。
-
diskCache顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取,它的直接操作对象为CurlCacheManager。它与memoryCache最大的区别在于,当退出进程时,内存中的数据会被清空,而磁盘的数据不会,所以,当下次再进入该进程时,该进程仍可以从diskCache中获得数据,而memoryCache则不行。
-
chrome的解释 Chrome employs two caches — an on-disk cache and a very fast in-memory cache. The lifetime of an in-memory cache is attached to the lifetime of a render process, which roughly corresponds to a tab. Requests that are answered from the in-memory cache are invisible to the web request API. If a request handler changes its behavior (for example, the behavior according to which requests are blocked), a simple page refresh might not respect this changed behavior. To make sure the behavior change goes through, call handlerBehaviorChanged() to flush the in-memory cache. But don't do it often; flushing the cache is a very expensive operation. You don't need to call handlerBehaviorChanged() after registering or unregistering an event listener.
- 大意是说chrome会使用两个缓存,一个是磁盘缓存一个是非常快的内存缓存,内存缓存是和渲染进程绑定的,大部分情况下于浏览器Tab对应。不要轻易强制刷新缓存,代价非常昂贵...
脚本文件的差异
- 红线圈出来的是不同的地方,难道是这样的吗?
- 发现一个不一样的,没有content-length字段
- 然后我又发现了一个不同的。而且这玩意在我强制刷新浏览器的情况下,还是
200 OK (from disk cache)
所以结论就是,留给大家思考吧。
参考文章:
- MDN Headers
- 浅谈浏览器http的缓存机制
- 浏览器缓存知识小结
- HTTP权威指南--缓存篇