别再看“协商缓存和强缓存”啦,你所知道的缓存知识可能全是错的!(一)

1,040 阅读8分钟

Intro: 因为这个我和面试官吵了起来

这次秋招里面试的时候,我和一位大厂面试官吵了起来。原因就是我认为他的“强缓存/协商缓存”根本不是HTTP里的概念,而是中文论坛里传来传去的概念。导致我认为面试官是个水货,而面试官觉得你为什么这么出名的概念都不知道,你是水货把?因为我本身本科就是在北美读的,不论是计网这门课还是相关的工作内容其实根本都没出现这两个对应的单词 但其实在英文社区里如果搜索

cache negotiation/ strong cache

事实上根本没有任何相关的文章。最接近的概念实际上是内容协商(content negotiation)和HTTP cache。而我合理怀疑一开始的那位作者应该是按照自己的理解随便糅合了一下HTTP官方的协议定义,自己创造了“强缓存”和“协商缓存”这种概念,接着被国内各种博客反复转载导致传播开来变成了所谓的“标准答案”,我自己看到的很多内容无法自圆其说更无法保证正确性,出处也没有注明(博客不标注reference真的是很不好的事情,难以追溯正确性)

我这里就把我当时学习缓存的知识重新整理,我尝试介绍一些背景和问题,这样再来谈缓存,我觉得会帮助大家理解为何要这么设计而不是毫无背景的死记硬背,不会设计特别复杂特别完整的流程图全部串起来,而是优先讲解每个小部分。

缓存类型

其实谈到HTTP缓存,我们知道缓存可以是浏览器的本地缓存这里,但其实另外的一部分是在网络的公共部分,能让别的客户端读取的。因此主要分为两类

  1. Local Cache 本地缓存 或者叫做私有缓存
  2. Shared Cache 公共缓存 或者共享缓存

image.png 出处 如图所示,在没有缓存的时候,所有内容请求直接在网络中打到服务器,服务器一一相应,这就是完全在网路中没有设置任何缓存的时候发生的。

而第二排 在中间加入了一个公共的缓存区域,可以是代理,可以是CDN,在这部分缓存就可以直接返回内容。实际到达服务器的请求只有第一次请求的时候。

而第二排第二个则是私有缓存,也就是“协商缓存/强缓存”存在的地方。通常是客户端浏览器进行本地的高速缓存。

效率上公共缓存>私有缓存>无缓存。

浏览器的缓存编年史

90年代 那时候是怎么缓存的?

Expires

讲一个故事,假如我们小明每天都有作业,妈妈每次都叫小明去学校问,今天的任务是什么。

第一天:妈妈问作业是啥 小明跑去学校,背回来10本课本读

第二天:妈妈问作业是啥 小明跑去学校,背回来一模一样的10本课本,但是旧的10本没啥用就丢了

第三天:妈妈问作业是啥 小明跑去学校,背回来一模一样的10本课本 还是把旧的丢了

第四天:妈妈又问小明,但是小明生气了!为什么每次学校都给一样的课本 我不累吗!!明明都是一样的课本,哪用得着天天去!还浪费书

这时候小明就去学校问,你们这课本到底要用几天?? 学校说,这一周7天都是这个课本。

以上 就是初代缓存的原型了。

于是最早的设计者就规定只要服务端(学校)告诉了浏览器(小明)这个资源能用多久,之后只要在请求的时间范围内那么你就直接存下来用。header Expires就此诞生!

来让我们看看 HTTP Response的header

Expires: Thu, 01 Dec 1994 16:00:00 GMT

这个header说明服务端返回告诉浏览器,在这个时间前,你就别再找我问这个资源了自己存好。在这个时机之前你的资源永远都是新鲜的(fresh)

image.png 这个方案看起来很不错 但是会有一些问题,一旦这些内容被浏览器缓存了,除非你自己手动的清除,不然普通的用户是永远会看到旧的内容,有的浏览器甚至存的时间会更长。这就导致了我们经常能发现很多网站修了bug之后要求用户,请手动清除缓存。这种方案通常来说是一种灾难。

用Last-Modified 和# If-Modified-Since校验去解决!

最近教育局突然换教材了,但是小明上次被通知教材是要用一个月的,结果害得小明一直用的旧课本。学校觉得这个方案不太好,所以让小明还是每天来学校一趟确认下是不是要用20年版本的课本,如果来新书了再把新书带回去就行。

HTTP设计者也发现,写死时间太不灵活导致了很多的问题。所以让HTTP的请求者主动去问询,做校验。

下面是HTTP的伪代码

if(你在2020年之后改了版){
    return [新书,新教材的时间]
}else{
    return 304 你继续用旧教材
}

=>
if(Modified-Since(2020)){
    return [file, Last-Modified]
}else{
    return 304 
}

简单讲就是服务端第一次请求时返回的Header带上了last-modified, 第二次请求的时候只要在header加上 last-modified-since =第一次返回的时间,服务器就会做一个比较是否有新的修改。假如那之后文件根本没有修改过,那么就把HTTP code设置为304.假如确实有更新,那么就返回新的文件并且更新下最后一次修改的时间。

image.png

简易流程图

启发式计算新鲜度

有一天,学校忘记告诉小明这个课本还要用多久了,也没告诉他第二天再去问问。小机灵鬼小明看了下,这个教材已经一年都没有换了,我也不去问了我就拍个脑袋猜一下距离上次改教材有一年了,那就10%的时间吧。未来36.5天我都懒得去了,如果老妈问起来我就直接告诉他好了。

以上这个小明其实就是自作主张的浏览器!

但是假如你发送的HTTP请求没有返回任何关于控制缓存的Header,或者是返回了重复的cache control/ expires

这个时候浏览器会做什么呢HTTP标准 告诉浏览器,既然没有人告诉你该怎么做或者指令有误,那你可以自己算出来一个“合理”的缓存时间。这叫做启发式计算:calculate heuristic freshness

这个 "合理 "的过期时间到底是如何确定的,大多数浏览器使用:(当前时间- last modified时间) *10%。比方说你请求了一个一年都没有修改的文件,那么他会储存36.5天。

一个例子: 下列的header并没有正确设置缓存策略

Date:	Fri, 06 Dec 2019 13:09:03 GMT
Etag: "c4238385fe77f826b5584fed1f1f1659"
Last-Modified: Tue, 03 Dec 2019 10:50:54 GMT

各个浏览器的计算方法其实并不一样,IE,网景当年并没有做出标准。所以造成了蛮多的灾难并且到现在的浏览器都有这个特性。

Pragma,请不要缓存

小明自作主张被发现了,于是学校惩罚小明每天都得来学校拿课本不准用旧的了。

当然我们也有很多场景和内容每次都不一样,根本不想让浏览器做任何的缓存,那么这个时候这个Header就有用了。

Pragma:no-cache

When the "no-cache" directive is present in a request message, an application should forward the request toward the origin server even if it has a cached copy of what is being requested.

根据HTTP1.0的规定,no-cache用于让客户端强制接收新文件并刷新内容的。

总结

HTTP1.0时代,大家刚开始探索如何做缓存,主要依靠的就是新鲜度和校验,又诞生了启发式计算让客户端乱来的这种错误设计,并额外的增加了pragma让客户端放弃校验。

在下一章HTTP 1.1中【可能鸽】,大家的缓存策略就逐渐迈入现代。在网速提升上去之后,大家对于大文件下载的需求比起90年代的拨号上网来说其实下载并没有那么重要了所以也发生了一系列的变化。

第一次写文,新手不易。希望大家多多点赞评论支持!

往期内容

# 毕业挣扎终于上岸大厂,2021 只是起点不是终点