2.6 Cache #
2.6.1 服务端缓存控制 #
服务器标记资源有效期使用的头字段是 “Cache-Control”,里面的值 “max-age=30” 就是资源的有效时间,相当于告诉浏览器,“这个页面只能缓存 30 秒,之后就算是过期,不能用。”
除了 “Cache-Control’”,服务器也可以用 “Expires” 字段来标记资源的有效期,它的形式和 Cookie 的差不多,同样属于 “过时” 的属性,优先级低于 “Cache-Control’”。
还有一个历史遗留字段 “Pragma:no-cache”,它相当于 “Cache-Control: no-cache”,除非为了兼容HTTP/1.0 否则不建议使用。
这里的 max-age 是 “生存时间”(又叫“新鲜度” “缓存寿命”,类似 TTL,Time-To-Live),时间的计算起点是响应报文的创建时刻(即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。
“max-age” 是 HTTP 缓存控制最常用的属性,此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存:
-
no-store:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面;
-
no-cache:它的字面含义容易与 no-store 搞混,实际的意思并不是不允许缓存,而是可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;
-
must-revalidate:又是一个和 no-cache 相似的词,它的意思是如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。
可以拿生鲜速递来举例说明一下:
-
no-store:买来的西瓜不允许放进冰箱,要么立刻吃,要么立刻扔掉;
-
no-cache:可以放进冰箱,但吃之前必须问超市有没有更新鲜的,有就吃超市里的;
-
must-revalidate:可以放进冰箱,保鲜期内可以吃,过期了就要问超市让不让吃。
2.6.2 客户端缓存控制 #
其实不止服务器可以发 “Cache-Control” 头,浏览器也可以发 “Cache-Control”,也就是说请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。
当点 “刷新” 按钮的时候,浏览器会在请求头里加一个 “Cache-Control: max-age=0”。因为 max-age 是 “生存时间”,max-age=0 的意思就是拿到最新的数据,而本地缓存里的数据至少保存了几秒钟,所以浏览器就不会使用缓存,而是向服务器发请求。服务器看到 max-age=0,也就会用一个最新生成的报文回应浏览器。
浏览器用 “Cache-Control” 做缓存控制只能是刷新数据,不能很好地利用缓存数据,又因为缓存会失效,使用前还必须要去服务器验证是否是最新版。所以 HTTP 协议就定义了一系列 “If” 开头的 “条件请求” 字段,专门用来检查验证资源是否过期,验证的责任交给服务器,浏览器只需 “坐享其成”。
2.6.3 条件请求 #
条件请求常用的是 “if-Modified-Since” 和 “If-None-Match” 这两个。需要第一次的响应报文预先提供 “Last-modified” 和 “ETag”,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。如果资源没有变,服务器就回应一个 “304 Not Modified”,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。
“Last-modified” 是文件的最后修改时间。
ETag 是 “实体标签”(Entity Tag)的缩写,是资源的一个唯一标识,主要是用来解决修改时间无法准确区分文件变化的问题。比如,一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分。再比如,一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽。使用 ETag 就可以精确地识别资源的变动情况,让浏览器能够更有效地利用缓存。
ETag 还有 “强” “弱” 之分。
强 ETag 要求资源在字节级别必须完全相符,弱 ETag 在值前有个 “W/” 标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变,例如 HTML 里的标签顺序调整,或者多了几个空格。
拿生鲜速递做比喻比较容易理解:
你打电话给超市,“我这个西瓜是 3 天前买的,还有最新的吗?”。超市看了一下库存,说:“没有啊,我这里都是 3 天前的。”于是你就知道了,再让超市送货也没用,还是吃冰箱里的西瓜吧。这就是 “if-Modified-Since” 和 “Last-modified”。
但你还是想要最新的,就又打电话:“有不是沙瓤的西瓜吗?”,超市告诉你都是沙瓤的(Match),于是你还是只能吃冰箱里的沙瓤西瓜。这就是 “If-None-Match” 和 “弱ETag”。
第三次打电话,你说 “有不是 8 斤的沙瓤西瓜吗?”,这回超市给了你满意的答复:“有个 10 斤的沙瓤西瓜”。于是,你就扔掉了冰箱里的存货,让超市重新送了一个新的大西瓜。这就是 “If-None-Match” 和 “强 ETag”。
源服务器在设置完 “Cache-Control” 后必须要为报文加上 “Last-modified” 或 “ETag” 字段。否则,客户端和代理后面就无法使用条件请求来验证缓存是否有效,也就不会有 304 缓存重定向。
2.6.4 缓存代理 #
要区分客户端上的缓存和代理上的缓存,可以使用两个新属性 “private” 和 “public”。“private” 表示缓存只能在客户端保存,是用户 “私有” 的,不能放在代理上与别人共享。而 “public” 的意思就是缓存完全开放,谁都可以存,谁都可以用。
比如登录论坛,返回的响应报文里用 “Set-Cookie” 添加了论坛 ID,这就属于私人数据,不能存在代理上,不然,别人访问代理获取了被缓存的响应就麻烦了。
缓存失效后的重新验证也要区分开(即使用条件请求 “Last-modified” 和 “ETag”),“must-revalidate” 是只要过期就必须回源服务器验证,而新的 “proxy-revalidate” 只要求代理的缓存过期后必须验证,客户端不必回源,只验证到代理这个环节就行了。
缓存的生存时间可以使用新的 “s-maxage”(s 是 share 的意思,注意 maxage 中间没有“-”),只限定在代理上能够存多久,而客户端仍然使用 “max-age”。
还有一个代理专用的属性 “no-transform”。代理有时候会对缓存下来的数据做一些优化,比如把图片生成 png、webp 等几种格式,方便今后的请求处理,而 “no-transform” 就会禁止这样做,不许 “偷偷摸摸搞小动作”。
这些新的缓存控制属性比较复杂,用 “便利店冷柜” 来举例好理解一些。
水果上贴着标签 “private, max-age=5”。这就是说水果不能放进冷柜,必须直接给顾客,保鲜期 5 天,过期了还得去超市重新进货。
冻鱼上贴着标签 “public, max-age=5, s-maxage=10”。这个的意思就是可以在冰柜里存 10 天,但顾客那里只能存 5 天,过期了可以来便利店取,只要在 10 天之内就不必再找超市。
排骨上贴着标签 “max-age=30, proxy-revalidate, no-transform”。因为缓存默认是 public 的,那么它在便利店和顾客的冰箱里就都可以存 30 天,过期后便利店必须去超市进新货,而且不能擅自把 “大排” 改成 “小排”。
客户端缓存控制 #
“max-stale” 的意思是如果代理上的缓存过期了也可以接受,但不能过期太多,超过 x 秒也会不要。
“min-fresh” 的意思是缓存必须有效,而且必须在 x 秒后依然有效。
比如,草莓上贴着标签 “max-age=5”,现在已经在冰柜里存了 7 天。如果有请求 “max-stale=2”,意思是过期两天也能接受,所以刚好能卖出去。
但要是 “min-fresh=1”,这是绝对不允许过期的,就不会买走。这时如果有另外一个菠萝是 “max-age=10”,那么 “7+1<10”,在一天之后还是新鲜的,所以就能卖出去。
有的时候客户端还会发出一个特别的 “only-if-cached” 属性,表示只接受代理缓存的数据,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个 504(Gateway Timeout)。