HTTP 缓存技术 - 强制缓存

48 阅读15分钟

HTTP 缓存技术 - 强制缓存

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第二十八天,点击查看活动详情

强制缓存指的是只要浏览器没有过期,就使用缓存进行返回,主动性在浏览器方。

比如下面的请求当中,使用了缓存进行返回,强缓存利用两个响应头部实现, 相对时间“Cache-Control” 以及 "Expire"绝对时间 两个字段。

在讲述Cache-Control之前我们先停一下,先来了解后面这个过时的东西Expires

Expires 有什么用? 这个字段的作用是设置一个特殊的时间,一旦超过这个时间,就会过期,简而言之就是所谓的绝对时间,比如我们设置时间为 Expires:Sat,13 May 2022 07:00:00 GMT,意味着一旦超过这个时间就会失效。

但是这个时间是存在问题的,虽然原始服务器的时间无法更改,但是 客户端时间是可以自由改动的,这样就会导致客户端时间和服务器时间不一致的缓存一致性问题,也就是the world(砸瓦鲁多)。

此外Expires日期时间必须是格林威治时间(GMT),而不能是本地时间,也不能随意指定日期格式,局限性比较大。

此外如果设置的Expires过期时间是固定时间,但是返回之前没有没有更新下一次过期时间,那么每一次客户端的请求都会进入到服务端,会加大服务端压力。

于是Cache-Control针对Expires的局限而被HTTP1.1新增。

如果同时有 Cache-Control 和 Expires 字段,Cache-Control的优先级高于 Expires ,所以通常情况下不建议使用 Expires,如果非要使用,建议用在静态资源上给资源设置绝对过期时间,或者作为双保险兼容所有HTTP1.0代理服务使用。

此外Cache-ControlExpires这两个字段的区别是 Cache-Control 字段的选项要多一些,Cache-Control 是HTTP1.1标准协议中出现并推荐使用的,Expires 是HTTP1.0的规定,但是HTTP1.0并不是明确标准。

HTTP 1.0 虽然明面上属于草稿纸协议,Expires看似也不推荐使用,但是后续的协议并没有废弃它,所以还是有一定的存在意义的(向前兼容)。

首部字段 Expires 会将资源失效的日期告知客户端。如果不希望资源被缓存,则在首部字段里面和首部字段Date相同。

强制缓存的使用策略

  • 第一次访问服务器资源,服务器会在返回资源的同时返回这两个字段,同时为这两个字段设置过期时间。
  • 浏览器第二次乃至更多次访问,首先比对 Cache-Control的时间是否过期,如果有就使用缓存,没有就重新请求
  • 再次请求会更新 Cache-Control,之后以此反复。

Cache-Control例子

例子网址:web.dev/i18n/en/htt…

Cache-Control价值解释
max-age=86400响应可以由浏览器和中间缓存缓存长达 1 天(60 秒 x 60 分钟 x 24 小时)。
private, max-age=600响应可以由浏览器(但不是中间缓存)缓存长达 10 分钟(60 秒 x 10 分钟)。
public, max-age=31536000响应可以由任何缓存存储 1 年。
no-store不允许缓存响应,并且必须在每次请求时全部获取。

Cache-Control 字段选项

  • max-age:此参数为高优先级,代表缓存的最大存活时间,单位为秒,其实时间为客户端接受响应的那一刻计算。
  • no-cache:浏览器在每次使用缓存之前都必须使用服务器重新验证。注意这个字段并不是禁用缓存的真正含义,这里暂时卖个关子,下文继续解释。
  • no-store:缓存不应该缓存任何客户端和服务端的内容,实际上的不使用缓存。和no-cache的区别是这个设置完全才是真实的不想用缓存。
  • public:表示资源可以由任何缓存进行缓存。
  • private:表示指定资源专属于特定用户,虽然依然可以缓存,但只能在客户端缓存,比如私有的网页响应由桌面浏览器缓存,不能给CDN进行加速。

注意在Cache-Control指定max-age的属性时候,比起首部字段Expires,会优先处理max-age。

在《HTTP权威指南》可以看到更多字段解释,上面仅仅列举一些常用字段。

重点关注 no-cache,很容易误解含义的一个属性。

s-maxage

有时候我们会看到下面的请求属性:s-maxage

它所表示的含义是覆盖max-age或者Expires头,但是仅适用于共享缓存 (比如各个代理),私有缓存会忽略它(private)。

Cache-Control案例

通过下面的案例可以看到,内容是从disk cache本地返回的,没有请求服务器。

强制缓存除了下面的表现形式 from disk cache 之外,还有一种情况是使用from memory cache进行返回,表示同样不会访问服务器,但是返回的内容是从内存中来的,并且因为是内存所以下一次访问的内

关于更多有关Cache-Control的内容可以看看下面的资料网站。

Cache-Control - HTTP | MDN (mozilla.org)

Expires 和 Cache-Control 两者对比

其实本质上区别并不是很大,只不过Expires 是 HTTP1.0 出现的,要比Cache-Control (HTTP1.1)出得早而已,并且Cache-Control本身就是为了替换Expires 而存在的。

虽然目前大部分网站都是支持HTTP1.1 的,但是如果碰到只能识别HTTP1.0 的服务器,则此字段依然有存在价值,所以这种写法是一种保证前后兼容的稳定写法而已。

Cache-Control 流程图

图来自老外的博客:# Prevent unnecessary network requests with the HTTP Cache

需要最后一步的Etag,作用是在协商缓存中作为判断依据,这部分内容会在下文将到

Cache-Control 流程图

no-cache VS no-store

“no store”请求指令指示缓存不能存储此请求或对其的任何响应的任何部分。

The "no-store" request directive indicates that a cache MUST NOT store any part of either this request or any response to it. This directive applies to both private and shared caches.

no-store 比较好理解,它是真正意义上的不使用缓存,含义是禁用中间代理(浏览器,CDN,缓存服务器、代理)缓存响应内容,行为类似非代理缓存服务,一旦碰到缓存就会删除。

“no cache”请求指令表示,如果未在源服务器上成功验证,缓存不得使用存储的响应来满足请求。

The "no-cache" response directive indicates that the response MUST NOT be used to satisfy a subsequent request without successful validation on the origin server.

上面是RFC协议的原话,这个定义非常容易误解,隐藏的含义是,实际上no cache 是会进行缓存的,什么时候缓存呢?在与原始服务器进行新鲜度再验证之前,缓存不能将其提供给客户端使用。而如果再度验证服务器没有对于内容进行更改,那么还是使用缓存数据进行处理。

简而言之就是一句话:如果服务器没有更新内容,那么就会缓存数据,否则需要重新请求和服务器进行验证比对

大多数人会理解错这一层含义,会误认为是“不接受服务器的缓存响应”,实际上它是会接受的。《HTTP权威指南》说这个首部使用 do-not-serve-from-cache-without-revalidation(不需要请求服务,直接用缓存,除非服务器重新验证) 这个名字会更恰当一些,这里建议也这样理解。

这里肯定又会问,你都no-cache了我怎么知道什么时候响应新内容呢?

先别急,这里有一套稍微复杂的判断机制:协商缓存,学东西一点一点来,我们接着看几个强制缓存的问题。

max-age=0 和 no-cache 等价吗?

这个问题比较偏门,但是作为面试题角度比较刁钻。两者的区别是max-age=0通常是告诉浏览器建议刷新缓存,max-age=0非强制性(Should) 的,no-cache要求强制和服务器进行验证才允许使用缓存,所以no-cache 是具有强制(MUST)性的。

但是怎么处理得看浏览器的设计,所以不考虑浏览器设计的因素下,可以认为行为比较一致。或者直接自信点:差不多。

什么样的请求方法会被缓存?

  • GET 请求通常具备缓存失效。
  • HEAD方法跟GET方法相同,只不过服务器响应时不会返回消息体,所以HEAD请求会被缓存。
  • PUT 无法被缓存。
  • POST 缓存在指定明确的过期请柬请求字段的时候可以使用,但是基本没有被实施。

HEAD 很容易被忽略,实际上它具备一定的作用:

  • 检查连接是否活跃(active)。
  • 检查网页是否存在改动。
  • 多用于获取RSS,网站标志等信息的场景使用。

缓存优先级判断

采用下面的顺序:

  1. 如果是共享缓存并且存在s-maxage,则为最高优先级。
  2. 检查是否存在max-age。
  3. 检查是否存在Expires,需要使用此时间减去响应字段的Date,算出来的数值就是有效期。

需要提醒这些时间是不会和时区产生影响的,因为返回的都是原始服务器的时间

但是在这里我们发现一个问题,如果上面条件都不满足,如果不存在到期时间咋办?

因为原始服务器的时间并不是总是可靠的,如果请求当中没有任何“新鲜度“(max-age、Expires等过期时间)设置,请求头部也没指定任何禁用缓存和任何限制,那么这时候需要用到heuristic expiration time

heuristic expiration time(中文说法:启发式缓存时间),在《HTTP权威指南》叫做试探性过期时间,其实是利用其他字段的算出一个“合理”的估计值(也就是 Last-Modified)。

关于计算的方法,在RFC规范柄中没有强制如何设计,而是在协议中给出下面这句话:

If the response has a Last-Modified header field (Section 2.2 of [RFC7232]), caches are encouraged to use a heuristic expiration value that is no more than some fraction of the interval since that time. A typical setting of this fraction might be 10%.

如果响应具有Last-Modified header的标头字段(RFC7232的第2.2节),则鼓励缓存使用启发式缓存时间值,该值会计算一个不超过自该时间起间隔的某个比例。该分数的典型设置可能为10%

这是嘛意思?没看懂呀,其实这里要联系RFC原文的前后文了,这里就不贴图片了。大致意思是在优先级判断中的第三点判断,有效期计算被定义为Expires-Date字段

如果服务端返回Last-Modified header ,则计算方式为 Date字段 - Last-Modified字段值

但是如果直接这样计算有可能会太长了,RFC给出悲观估计 10 % 的建议值。目前的浏览器实现通常按照下面两点考虑:

  • 很久之前存放的文档一般不会更改,所以留在缓存很安全,悲观估计值的比率尚且可以接受(?)。
  • 频繁更新的内容通常缓存收益很小,使用悲观估计值10%可以尽可能的减少缓存时间,尽可能的返回最新内容。

更新频率不同长短的资源都能收益,这样看起来这处理方式是不是很不错呀?但是 想法是好的,现实是这样做会带来更多麻烦,这点放到下面讨论。

总而言之,不满足缓存优先级判断,浏览器通常会用 Last-Modified字段值 计算一个合适的参考值作为缓存过期时间存在,最终的计算公式为:

Last-Modified Time - Date * 0.1 (10%)`

当存储的响应中存在显式过期时间时,缓存不得使用启发式缓存时间来确定新鲜度。不能使用此算法。官方这话是在暗示你要尽量给资源设置缓存过期时间,因为我建议的这东西不是特别靠谱。

目前多数浏览器使用的 LM-factor 算法(也就是上面的公式),但是目前的RFC协议建议这个值为 10 %,20% 这个说法现在来看已经过时。

10%以及悲观估计依据: This specification does not provide specific algorithms, but does impose worst-case constraints on their results. 本规范未提供具体算法,但对其结果施加了最坏情况约束,所谓的最坏情况约束就是 10%。

113 响应状态码:

这里有个偏门的 113 响应状态码,表示如果缓存使用了超过24小时的有效时间并且响应时间大于24小时,不应该采取任何操作。1XX状态码需要后续的确认操作。

但是实际上很多浏览器压根没有搭理过这个建议,也没有做响应措施,这一点需要注意。

如果Last-Modified都没有怎么办?

乖乖,如果这都没有的话,缓存通常会为没有任何新鲜周期线索的文档分配一个默认的新鲜周期(通常是一个小时或一天)。

有时比较保守的缓存会将这种试探性新鲜生存期设置为 0,强制缓存在每次将其提供给 客户端之前,都去验证一下这些数据仍然是新鲜的。

启发式缓存时间坏处

但是凡事都有例外,启发式缓存时间本意是好的,但是存在明显的弊端。

比如假如一个文件超过一个月没设置过期时间,并且已经经过一个月的时间,这时候发现文件存在严重问题,需要立刻修复。

这时候一旦修改,会导致上个版本3天之后才过期(1个月的10%=3天左右),意味着更新一个文件需要至少3天(20%就是将近一周),请求才会传新文件。如果使用CDN,这个时间还会更长。

当然这种问题解决方案也很多,比如在设置文件的时候带上版本号或者编号,比如对外进行 302 临时重定向到另一个位置并且设置过期时间,或者先删文件再后重新添加,并且手动强制同步。

综上所述,尽量不要使用启发式缓存,尽量给每个请求设置过期时间,但是也不要设置过长时间,强缓存会因为CDN等缓存服务器的关系导致一个资源迟迟难以更新(哪怕没有启发式缓存时间)

和RFC2616的改动

注:[RFC2616]第13.9节禁止缓存计算带有查询组件的URI的启发式新鲜度(即包含“?”的URI)。此项在实践中,这并没有得到广泛推广。

因此,如果源服务器希望排除缓存,则鼓励它们发送显式指令(例如,缓存控制:无缓存)。

新响应的计算方式

下面这个公式是判断依据:

response_is_fresh = (freshness_lifetime > current_age)

判断依据十分简单,新鲜度的时间是否超过寿命,超过寿命就需要丢弃缓存重新请求。也正是因为这种简单粗暴的手法,使得缓存既能够提供便利的同时,不至于对于用户访问造成过多影响。

响应表头设置建议

因为启发式缓存时间的存在,建议重要的文件资源都加上缓存有效期。针对缓存有效期的响应头设计,通常有下面的几点建议:

版本化URL

比如针对CSS文件设置了长达一年的缓存过期时间,如果出现临时更改,有的用户如果刚好清除缓存可以看到最新的内容,而没有清除缓存的可能拿到本地旧版本文件。这样可以有更好的用户体验,旧缓存数据的用户在刷新缓存之后就可以看到新内容。

通常情况下,在文件名中嵌入文件的版本号来执行此操作,例如style.x234dff.css

无版本化URL

如果是没有版本化的URL很久突然要进行更新,则需要尽量添加头部。

Cache-Control值可以帮助我们微调未版本化 URL 的缓存位置和方式:

  • no-cache:缓存请求当前URL的版本数据之前需要和服务器进行验证。
  • no-store:屏蔽中间代理服务器的缓存行为,不存储缓存文件。
  • private:浏览器可以缓存文件,但是中间代理缓存服务器不能缓存。
  • public:响应可以被任何缓存进行存储。