看完这篇还不清楚缓存,求你打我😈(有彩蛋)

21,438 阅读23分钟

心理引导

你是不是看了很多有关缓存的文章,然后都有个大概理解。然后去面试的时候,说起来还是有点结结巴巴咩?被问的慌慌张张?面完心里也没底😶。我把onenote上的笔记拿出来分享给大家咩。当然啦,为了文章内容的可靠性,尽可能使用专业用语。依赖《http权威指南》描述相关内容。至于彩蛋嘛,慢慢翻哦~

倒不是因为no-cache,Etag,expires,If-Modified-Since,这些字眼困扰我们,其实如果http机制没有上岸,让你去实现这个交涉过程,你也会和对方相互约定相关制约。只是在原本不完善的基础上,一点一点优化,于是发展成现在这样。自己写的代码从零到有再到优化,同样的道理,让同事来看你的代码不也是一样的感受(什么JB玩意儿,shit)。所以啊,别怕麻烦,耐心一点看(一次看不完收藏下来,下次接着看嘻嘻😁)。 

其实web缓存无非就是:数据库缓存、服务器端缓存(代理服务器缓存、CDN 缓存)、浏览器缓存。这里呢,我们先讲浏览器缓存里的http缓存

为什么要缓存?

每天早上设置闹钟准时在支付宝的蚂蚁森林收(tou)能量,手里这台用了4年多的苹果6s,特别慢,每次进入一个好友主页都要加载半天,同事A(和我一样的手机)说这网络好差,同事B的苹果XS秒进,近乎看不见延迟。其实这里网络无辜背锅,手机配置性能跟不上了啊,就像家里那台04年的台式机,卡的像机器人。当然这么快,缓存没有功劳的吗。这就是生活中最典型的案例,也是直接影响到每天收能量的速度!

如果可以,评论区留下支付宝账号,互割韭菜啊!!😍

所以啊,缓存解决哪些问题呢?

  • 缓存减少了冗余的数据传输,节省了你的网络费用。
  • 缓存缓解了网络瓶颈的问题。不需要更多的带宽就能够更快地加载页面。
  • 缓存降低了对原始服务器的要求。服务器可以更快地响应,避免过载的出现。
  • 缓存降低了距离时延,因为从较远的地方加载页面会更慢一些。 
我去手机上可以买火车票,为什么还要开车去火车站买呢?闲的蛋疼啊,不如来把王者。

当然缓存无法保存世界上每份文档的副本 ,可以用已有的副本为某些到达缓存的请求提供服务。这被称为缓存命中(cache hit),其他一些到达缓存的请求可能会由于没有副本可用,而被转发给原始服务器。这被称为缓存未命中(cache miss) 。如下所示:


纵览缓存处理步骤

1.第一步:接收------缓存从网络中读取抵达的请求报文

缓存检测到一条网络连接上的活动,读取输入数据。高性能的缓存会同时从多条输入连接上读取数据,在整条报文抵达之前开始对事务进行处理。 

2.第二步:解析------缓存对报文进行解析,提取出 URL 和各种首部

接下来,缓存将请求报文解析为片断,将首部的各个部分放入易于操作的数据结构中。这样,缓存软件就更容易处理首部字段并修改它们了 。

解析程序还要负责首部各部分的标准化,将大小写或可替换数据格式之类不太重要的区别都看作等 效的。而且,某些请求报文中包含有完整的绝对 URL,而其他一些请求中包含的则是相对 URL 和 Host 首部,所以解析程序通常都要将这些细节隐藏起来 。

3.第三步:查询------缓存查看是否有本地副本可用,如果没有,就获取一份副本(并将其保存在本地) 

在第三步中,缓存获取了 URL,查找本地副本。本地副本可能存储在内存、本地磁盘,甚至附近的另一台计算机中。专业级的缓存会使用快速算法来确定本地缓存中是否有某个对象。如果本地没有这个文档,它可以根据情形和配置,到原始服务器 或父代理中去取,或者返回一条错误信息。

已缓存对象中包含了服务器响应主体和原始服务器响应首部,这样就会在缓存命中时返回正确的服务器首部。已缓存对象中还包含了一些元数据(metadata),用来记 录对象在缓存中停留了多长时间,以及它被用过多少次等 。

复杂的缓存还会保留引发服务器响应的原始客户端响应首部的一份副本,以用于 HTTP/1.1 内容协商。 

4.第四步:新鲜度检测------缓存查看已缓存副本是否足够新鲜,如果不是,就询问服务器是否有任何更新 

HTTP 通过缓存将服务器文档的副本保留一段时间。在这段时间里,都认为文档是 “新鲜的”,缓存可以在不联系服务器的情况下,直接提供该文档。但一旦已缓存副 本停留的时间太长,超过了文档的新鲜度限值(freshness limit),就认为对象“过时”了,在提供该文档之前,缓存要再次与服务器进行确认,以查看文档是否发生了变化。客户端发送给缓存的所有请求首部自身都可以强制缓存进行再验证,或者完全避免验证,这使得事情变得更加复杂了。

HTTP 有一组非常复杂的新鲜度检测规则,缓存产品支持的大量配置选项,以及与非HTTP 新鲜度标准进行互通的需要则使问题变得更加严重了。

5.第五步:创建响应------缓存会用新的首部和已缓存的主体来构建一条响应报文

我们希望缓存的响应看起来就像来自原始服务器的一样,缓存将已缓存的服务器响应首部作为响应首部的起点。然后缓存对这些基础首部进行了修改和扩充。

缓存负责对这些首部进行改造,以便与客户端的要求相匹配。比如,服务器返回 的可能是一条 HTTP/1.0 响应(甚至是 HTTP/0.9 响应),而客户端期待的是一条 HTTP/1.1 响应,在这种情况下,缓存必须对首部进行相应的转换。缓存还会向其中 插入新鲜度信息(Cache-Control、Age 以及 Expires 首部),而且通常会包含一 个 Via 首部来说明请求是由一个代理缓存提供的。

注意,缓存不应该调整 Date 首部。Date 首部表示的是原始服务器最初产生这个对象的日期。 

6.第六步:发送------缓存通过网络将响应发回给客户端 

一旦响应首部准备好了,缓存就将响应回送给客户端。和所有代理服务器一样,代 理缓存要管理与客户端之间的连接。高性能的缓存会尽力高效地发送数据,通常可以避免在本地缓存和网络 I/O 缓冲区之间进行文档内容的复制。 

7.第七部:日志------缓存可选地创建一个日志文件条目来描述这个事务 

大多数缓存都会保存日志文件以及与缓存的使用有关的一些统计数据。每个缓存事务结束之后,缓存都会更新缓存命中和未命中数目的统计数据(以及其他相关的度 量值),并将条目插入一个用来显示请求类型、URL 和所发生事件的日志文件。 

说这么多不如来一张图,清晰客观:


强缓存

通过特殊的 HTTP Cache-Control 首部和 Expires 首部,HTTP 让原始服务器向 每个文档附加了一个“过期日期”。就像一夸脱牛奶上的过期日期一样,这些首部说明了在多长时间内可以将这些内容视为新鲜的。 


服务器用 HTTP/1.0+ 的 Expires 首部或 HTTP/1.1 的 Cache-Control: max-age 响应首 部来指定过期日期,同时还会带有响应主体。Expires 首部和 Cache-Control: max-age 首部所做的事情本质上是一样的,但由于 Cache-Control 首部使用的是 相对时间而不是绝对日期,所以我们更倾向于使用比较新的 Cache-Control 首部。 绝对日期依赖于计算机时钟的正确设置。 

即Cache-Control优先级更高。

服务器再验证

仅仅是已缓存文档过期了并不意味着它和原始服务器上目前处于活跃状态的文档有 实际的区别;这只是意味着到了要进行核对的时间了。这种情况被称为“服务器再 验证”,说明缓存需要询问原始服务器文档是否发生了变化。 

  • 如果再验证显示内容发生了变化,缓存会获取一份新的文档副本,并将其存储在旧文档的位置上,然后将文档发送给客户端。

  • 如果再验证显示内容没有发生变化,缓存只需要获取新的首部,包括一个新的过期日期,并对缓存中的首部进行更新就行了。 


我们来看一下Cache-Control的头部:(不一定非要一下子全部记住,每次回想记不清的时候来看一眼,一次记一个,就能全部get了!🏆)

1)Cache-Control:no-store

禁止一切缓存(这个才是响应不被缓存的意思)。缓存通常会像非缓存代理服务器一样,向客户端转发一条 no-store 响应,然后删除对象。 

2)Cache-Control:no-cache

强制客户端直接向服务器发送请求,也就是说每次请求都必须向服务器发送。服务器接收到请求,然后判断资源是否变更,是则返回新内容,否则返回304,未变更。这个很容易让人产生误解,使人误以为是响应不被缓存。实际上Cache-Control: no-cache是会被缓存的,只不过每次在向客户端(浏览器)提供响应数据时,缓存都要向服务器评估缓存响应的有效性。

HTTP/1.1 中提供 Pragma: no-cache 首部是为了兼容于 HTTP/1.0+。除了与只理解 Pragma: no-cache 的 HTTP/1.0 应用程序进行交互时,HTTP 1.1 应用程序都应该使用 Cache-Control: no-cache。 从技术上来讲,Pragma:no-cache 首部只能用于 HTTP 请求,但在实际中它作为扩展首部已被广 泛地用于 HTTP 请求和响应之中。 

3)Cache-Control:must-revalidate

在事先没有跟原始服务器进行再验证的情况下,不能提供这个对象的陈旧副本。缓存仍然可以随意提供新鲜的副本。如果在缓存进行 must-revalidate 新鲜度检查时,原始服务器不可 用,缓存就必须返回一条 504 Gateway Timeout 错误。

4)Cache-Control:max-age

首部表示的是从服务器将文档传来之时起,可以认为此 文档处于新鲜状态的秒数。

5)Cache-Control:s-maxage

和max-age是一样的,不过它只针对代理服务器缓存而言。

6)Cache-Control:private

只能针对个人用户,而不能被代理服务器缓存。

7)Cache-Control:public

可以被任何对象缓存,包括发送请求的客户端,代理服务器。

8)Cache-Control:max-stale

缓存可以随意提供过期的文件。如果指定了参数 <s>,在这段 时间内,文档就不能过期。这条指令放松了缓存的规则。

通过HTTP-EQUIV控制HTML缓存

HTML 2.0 定义了 <META HTTP-EQUIV> 标签。这个可选的标签位于 HTML 文档的顶部,定义了应该与文档有所关联的 HTTP 首部。

<HTML>
    <HEAD>
	<TITLE>My Document</TITLE>

	<META HTTP-EQUIV="Cache-control" CONTENT="no-cache">
    </HEAD>
    ... 			
</HTML>		
HTTP 服务器可以用此信息来处理文档。特别是,它可以在为请求此文档的报文所发送的响应中包含一个首部字段:首部名称是从 HTTP-EQUIV 属性值中获取的,首部值是从CONTENT 属性值中获取的。 

不幸的是,支持这个可选特性会增加服务器的额外负载,这些值也只是静态的,而 且它只支持 HTML,不支持很多其他的文件类型,所以很少有 Web 服务器和代理支 持此特性。 

总之,<META HTTP-EQUIV> 标签并不是控制文档缓存特性的好方法。通过配置正 确的服务器发出 HTTP 首部,是传送文档缓存控制请求的唯一可靠的方法。 


tips:

 Web 浏览器都有 Refresh(刷新)或 Reload(重载)按钮,可以强制对浏览器或 代理缓存中可能过期的内容进行刷新。Refresh 按钮会发布一个附加了 Cache- Control 请求首部的 GET 请求,这个请求会强制进行再验证,或者无条件地从服 务器获取文档。Refresh 的确切行为取决于特定的浏览器、文档以及拦截缓存的配置。

借老哥一张图:理解web缓存


条件方法再验证(协商缓存)

If-Modified-Since:Date再验证 

如果从指定日期之后文档被修改过了,就执行请求的方法。可以与 Last-Modified 服务器响应首部配合使用,只有在内容被修改后与已缓存版本有所不同的时候才去获取内容。

  • 如果自指定日期后,文档被修改了,If-Modified-Since 条件就为真,通常 GET 就会成功执行。携带新首部的新文档会被返回给缓存,新首部除了其他信息之外,还包含了一个新的过期日期。

  • 如果自指定日期后,文档没被修改过,条件就为假,会向客户端返回一个小的304 Not Modified 响应报文,为了提高有效性,不会返回文档的主体。16 这 些首部是放在响应中返回的,但只会返回那些需要在源端更新的首部。比如, Content-Type 首部通常不会被修改,所以通常不需要发送。一般会发送一个新的过期日期。

  • If-Modified-Since 首部可以与 Last-Modified 服务器响应首部配合工作。原始 服务器会将最后的修改日期附加到所提供的文档上去。当缓存要对已缓存文档进行 再验证时,就会包含一个 If-Modified-Since 首部,其中携带有最后修改已缓存副本的日期:

    如果未发生变化,If-Modified-Since 再验证会返回 304 响应,如果发生了变化, 就返回带有新主体的 200 响应。


If-None-Match:etag

ETag 实体标签: 一般为资源实体的哈希值,即ETag就是服务器生成的一个标记,用来标识返回值是否有变化。且Etag的优先级高于Last-Modified。

 服务器可以为文档提供特殊的标签,而不是将其与最近修改日期相匹配,这些标签就像序列号一样。如果已缓存标签与服务器文档中的标签有所不同,If-None-Match 首部就会执行所请求的方法。

灵魂拷问, 为什么有了If-Modified-Since还用If-None-Match😇?

  • 有些文档可能会被周期性地重写(比如,从一个后台进程中写入),但实际包含的数据常常是一样的。尽管内容没有变化,但修改日期会发生变化。

  • 有些文档可能被修改了,但所做修改并不重要,不需要让世界范围内的缓存都重装数据(比如对拼写或注释的修改)。

  • 有些服务器无法准确地判定其页面的最后修改日期。

  • 有些服务器提供的文档会在亚秒间隙发生变化(比如,实时监视器),对这些服务器来说,以一秒为粒度的修改日期可能就不够用了。 

当你第一次发起HTTP请求时,服务器会返回一个Etag,并在你第二次发起同一个请求时,客户端会同时发送一个If-None-Match,而它的值就是Etag的值(此处由发起请求的客户端来设置)。缓存中有一个实体标签为Etag: v2.6 的文档。它会与原始服务器进行再验证,如果标签Etag: v2.6 不再匹配,就会请求一个新对象。假设标签仍然与之 匹配,因此会返回一条 304 Not Modified 响应。 如果服务器上的实体标签已经发生了变化(可能变成了 Etag: v3.0),服务器会在一个 200OK 响应中返回新的内容以及相应的新 Etag。

举个例子:

当你第一次发起HTTP请求时,服务器会返回一个Etag


并在你第二次发起同一个请求时,客户端会同时发送一个If-None-Match,而它的值就是Etag的值(此处由发起请求的客户端来设置)。


大部分人都不知道的一点:

 有时候,客户端和服务器可能需要采用不那么精确的实体标记验证方法。例如,某服务器可能想对一个很大、被广泛缓存的文档进行一些美化修饰,但不想在缓存服务器再验证时产生很大的传输流量。在这种情况下,该服务器可以在标记前面加上“W/”前缀来广播一个“弱”实体标记。对于弱实体标记来说,只有当关联的实体在语义上发生了重大改变时,标记才会变化。而强实体标记则不管关联的实体发生 了什么性质的变化,标记都一定会改变。

下面的例子展示了客户端如何用弱实体标记向服务器请求再验证。服务器仅当文档 的内容从版本 4.0 算起发生了显著变化时,才返回主体:

 GET /announce.html HTTP/1.1 

 If-None-Match: W/"v4.0"

概括一下,当客户端多次访问同一个资源时,首先需要判断它当前的副本是不是仍然新鲜。如果不再新鲜,它们就必须从服务器获取最新的版本。为了避免在资源没有改变的情况下收到一份相同的副本,客户端可以向服务器发送有条件的请求,说明能唯一标识客户端当前副本的验证码。只在资源和客户端的副本不同的情况下服务器才会发送其副本。

比如简书就有类似的例子:


所以,彩蛋到底是啥?

昨天问旁边大佬讨论关于登录状态,除了cookie和token,还有其他方式吗?

因为csrf可以拿cookie的值,xss可以拿token的值,万一某个网站存在这两个渗透攻击,不就完蛋了。


正好这次看到了一些方法,(实际也不好使😆,但也是思路)

  • 承载用户身份信息的 HTTP 首部。

  • 客户端 IP 地址跟踪,通过用户的 IP 地址对其进行识别。

  • 用户登录,用认证方式来识别用户。

  • 胖 URL,一种在 URL 中嵌入识别信息的技术。 

From:用户的 E-mail 地址

From 首部包含了用户的 E-mail 地址。每个用户都有不同的 E-mail 地址,所以在理 想情况下,可以将这个地址作为可行的源端来识别用户。但由于担心那些不讲道德 的服务器会搜集这些 E-mail 地址,用于垃圾邮件的散发,所以很少有浏览器会发送 From 首部。实际上,From 首部是由自动化的机器人或蜘蛛发送的,这样在出现问 题时,网管还有个地方可以发送愤怒的投诉邮件。 

 User-Agent:用户的浏览器软件 

User-Agent 首部可以将用户所用浏览器的相关信息告知服务器,包括程序的名称 和版本,通常还包含操作系统的相关信息。要实现定制内容与特定的浏览器及其属 性间的良好互操作时,这个首部是非常有用的,但它并没有为识别特定的用户提供 太多有意义的帮助。 

Referer :用户是从这个页面上依照链接跳转过来的

Referer 首部提供了用户来源页面的 URL。Referer 首部自身并不能完全标识用户,但它确实说明了用户之前访问过哪个页面。通过它可以更好地理解用户的浏览行为,以及用户的兴趣所在。 

Authorization:用户名和密码

为了使 Web 站点的登录更加简便,HTTP 中包含了一种内建机制,可以用 WWW- Authenticate 首部和 Authorization 首部向 Web 站点传送用户的相关信息。一 旦登录,浏览器就可以不断地在每条发往这个站点的请求中发送这个登录信息了, 这样,就总是有登录信息可用了。 

Client-IP:客户端的 IP 地址

X-Forwarded-For: 客户端的 IP 地址 

如果每个用 户都有不同的 IP 地址,IP 地址(如果会发生变化的话)也很少会发生变化,而且 Web 服务器可以判断出每条请求的客户端 IP 地址的话,这种方案是可行的。通常 在 HTTP 首部并不提供客户端的 IP 地址, 但 Web 服务器可以找到承载 HTTP 请求 的 TCP 连接另一端的 IP 地址。 

但是,使用客户端 IP 地址来识别用户存在着很多缺点,限制了将其作为用户识别技 术的效能。 

  • 客户端 IP 地址描述的是所用的机器,而不是用户。如果多个用户共享同一台计 算机,就无法对其进行区分了。

  • 很多因特网服务提供商都会在用户登录时为其动态分配 IP 地址。用户每次登录 时,都会得到一个不同的地址,因此 Web 服务器不能假设 IP 地址可以在各登录 会话之间标识用户。

  • 为了提高安全性,并对稀缺的地址资源进行管理,很多用户都是通过网络地址转 换(Network Address Translation,NAT)防火墙来浏览网络内容的。这些 NAT 设备隐藏了防火墙后面那些实际客户端的 IP 地址,将实际的客户端 IP 地址转换 成了一个共享的防火墙 IP 地址(和不同的端口号)。

  • HTTP 代理和网关通常会打开一些新的、到原始服务器的 TCP 连接。Web 服务 器看到的将是代理服务器的 IP 地址,而不是客户端的。有些代理为了绕过这个 问题会添加特殊的 Client-IP 或 X-Forwarded-For 扩展首部来保存原始的 IP 地址(参见图 11-1)。但并不是所有的代理都支持这种行为。 

胖URL

有些 Web 站点会为每个用户生成特定版本的 URL 来追踪用户的身份。通常,会对 真正的 URL 进行扩展,在 URL 路径开始或结束的地方添加一些状态信息。用户浏 览站点时,Web 服务器会动态生成一些超链,继续维护 URL 中的状态信息。 

改动后包含了用户状态信息的 URL 被称为胖 URL(fat URL)。下面是 Amazon.com 电子商务网站使用的一些胖 URL 实例。每个 URL 后面都附加了一个用户特有的标 识码(在这个例子中就是 002-1145265-8016838),这个标识码有助于在用户浏览商 店内容时对其进行跟踪。

 ...		
<a href="/exec/obidos/tg/browse/-/229220/ref=gr_gifts/002-1145265-8016838">All Gifts</a><br>
<a href="/exec/obidos/wishlist/ref=gr_pl1_/002-1145265-8016838">WishList</a><br>
...
<a href="http://s1.amazon.com/exec/varzea/tg/armed-forces/-//ref=gr_af_/002-1145265-8016838">Salute Our Troops</a><br>
<a href="/exec/obidos/tg/browse/-/749188/ref=gr_p4_/002-1145265-8016838">Free Shipping</a><br>
<a href="/exec/obidos/tg/browse/-/468532/ref=gr_returns/002-1145265-8016838">Easy Returns</a>
... 
可以通过胖 URL 将 Web 服务器上若干个独立的 HTTP 事务捆绑成一个“会话”或 “访问”。用户首次访问这个 Web 站点时,会生成一个唯一的 ID,用服务器可以识 别的方式将这个 ID 添加到 URL 中去,然后服务器就会将客户端重新导向这个胖 URL。不论什么时候,只要服务器收到了对胖 URL 的请求,就可以去查找与那个用 户 ID 相关的所有增量状态(购物车、简介等),然后重写所有的输出超链,使其成为胖 URL,以维护用户的 ID。

但这种技术存在几个很严重的问题:

  • 无法共享 URL
    胖 URL 中包含了与特定用户和会话有关的状态信息。如果将这个 URL 发送给其 他人,可能就在无意中将你积累的个人信息都共享出去了。

  • 破坏缓存
    为每个 URL 生成用户特有的版本就意味着不再有可供公共访问的 URL 需要缓存了。

  • 额外的服务器负荷
    服务器需要重写 HTML 页面使 URL 变胖。

  • 逃逸口
    用户跳转到其他站点或者请求一个特定的 URL 时,就很容易在无意中“逃离” 胖 URL 会话。只有当用户严格地追随预先修改过的链接时,胖 URL 才能工作。 如果用户逃离此链接,就会丢失他的进展(可能是一个已经装满了东西的购物 车)信息,得重新开始。

  • 在会话间是非持久的
    除非用户收藏了特定的胖 URL,否则用户退出登录时,所有的信息都会丢失。 

  • 丑陋的 URL  

    浏览器中显示的胖 URL 会给新用户带来困扰。 


it's over.

有问题,欢迎指正呀~