HTTP补全计划-HTTP缓存管理

496 阅读7分钟

为什么要做缓存

HTTP本身特性

由于HTTP本身就是无状态的的协议,而且一开始的HTTP协议并没有考虑到资源缓存的问题。但是随着HTTP协议运用越来越广泛,对于资源 的缓存可以提高客户端的响应度,提升整个系统的性能,因此HTTP不断推出新的缓存策略。

缓存的前端应用场景

缓存是提高性能的最好方式。对于一些资源,如果是读取多于写入,就可以适当使用缓存来提高性能。如前端领域的资源文件,大部分场景下, 都是读取多于写入,因此可以缓存一些常见的前端资源,如脚本,样式文件和图片资源等,来减少网络传输,让页面更具有响应性。当然,前端 的缓存是分布在不同的层级,小到代码层面的数据缓存,大到整个页面资源的缓存。此文主要是讲解前端资源的缓存。

缓存什么资源

文档相关资源

前端的资源可以分为以下几类:

  1. JS,CSS, Doc,Minifest (页面描述性相关)
  2. Fetch/XHR,WS,Wasm (接口相关)
  3. Img,Media,Font (外部资源相关)

而缓存这些不同类型的资源,需要有不同的缓存策略。具体的缓存策略根据不同的浏览器实现来定。

如何保持缓存的有效性

缓存模式

Cache Aside 模式

这是最常用的设计模式了,其具体逻辑如下。 失效:应用程序先从 Cache 取数据,如果没有得到,则从数据库中取数据,成功后,放到缓存中。 命中:应用程序从 Cache 中取数据,取到后返回。 更新:先把数据存到数据库中,成功后,再让缓存失效。

Read/Write Through 更新模式

Read Through 套路就是在查询操作中更新缓存。而Write Through 套路和 Read Through 相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由 Cache 自己更新数据库(这是一个同步操作)。

Write Behind Caching

Write Behind 又叫 Write Back。Write Back,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。性能最佳,但是数据一致性的保证比较弱。

不同HTTP版本的缓存控制策略

HTTP1.0

Pragma

Pragma: no-cache

与 Cache-Control: no-cache 效果一致。强制要求缓存服务器在返回缓存的版本之前将请求提交到源头服务器进行验证。 Pragma 是HTTP/1.0标准中定义的一个header属性,请求中包含Pragma的效果跟在头信息中定义Cache-Control: no-cache相同。一般用于向后兼容基于HTTP/1.0的客户端。

HTTP1.1

Expires

Expires 响应头包含日期/时间, 即在此时候之后,响应过期,如果值为0,则缓存过期。Expires 其实是一个时间戳,这里只不过是格式化的显示。在我们再次向服务器请求相同的资源时,浏览器就会先对比本地时间和 Expires 的时间戳,如果本地时间小于 Expires 设定的过期时间,那么就直接去缓存中取这个资源。但是由于此头部信息要求客户端和服务器的时间要强一致性,而且根据时间判断也不够准确,慢慢被后面的的Cache-Control取代了。

20220401002735

Cache-Control

通用消息头字段,通过指定的指令来实现缓存机制。指令是单向的。分为客户端和服务器两部分。 具体指令的意思如下表:

指令名作用
max-age=<seconds>设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间
max-stale[=<seconds>]表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间
min-fresh=<seconds>表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应
must-revalidate一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求
no-cache在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)
no-store缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存
no-transform不得对资源进行转换或转变。Content-Encoding、Content-Range、Content-Type等HTTP头不能由代理修改。
only-if-cached表明客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝。
private表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容
proxy-revalidate与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略
public表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容
s-maxage覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它
stale-if-error表示如果新的检查失败,则客户愿意接受陈旧的响应。秒数值表示客户在初始到期后愿意接受陈旧响应的时间(实验性)
stale-while-revalidate表明客户端愿意接受陈旧的响应,同时在后台异步检查新的响应。秒值指示客户愿意接受陈旧响应的时间长度。
  1. 客户端
Cache-Control: max-age=<seconds>
Cache-Control: max-stale[=<seconds>]
Cache-Control: min-fresh=<seconds>
Cache-control: no-cache
Cache-control: no-store
Cache-control: no-transform
Cache-control: only-if-cached
  1. 服务器
Cache-control: must-revalidate
Cache-control: no-cache
Cache-control: no-store
Cache-control: no-transform
Cache-control: public
Cache-control: private
Cache-control: proxy-revalidate
Cache-Control: max-age=<seconds>
Cache-control: s-maxage=<seconds>

而在cache-control主要是在新鲜度这块做工作,主要的指令都与此相关。要保证文件的新鲜度。从缓存时间和缓存内容来管理缓存。

  • 缓存时间

    利用相对的时间单位,如max-age,来指定客户端和中间代理的资源有效性的时间。默认采用时间来判断.

    response_is_fresh = (freshness_lifetime > current_age)
    
  • 缓存内容

    使用摘要算法,生成文件的指纹,如Etag,实现缓存内容未变更的情况下保持新鲜度。

具体的缓存判断流程如下图:

20220403110623

Vary

Vary HTTP 响应头决定了对于后续的请求头,如何判断是请求一个新的资源还是使用缓存的文件。

当缓存服务器收到一个请求,只有当前的请求和原始(缓存)的请求头跟缓存的响应头里的Vary都匹配,才能使用缓存的响应。

浏览器缓存数据,一般用key-value形式,而这个key一般是URL,而这个vary,相当于key值变为URL+vary的形式,也就是说明相同的URL,带上不同的vary,就可以提供不同的缓存资源了,比如手机端和桌面端都是使用同一个url,这时候加上vary: user-agent,就可以返回不同的资源回来了。而为啥淘宝啥的没有这个,是因为淘宝手机端和桌面端的域名不同,所以就没必要加上这个vary: user-agent。

淘宝手机端的Doc的HTTP请求中,vary的取值是Accept-Encoding, Ali-Detector-Type。因此,如果是不同的encoding,则会用不同的缓存响应,防止客户端解码失败,而Ali-Detector-Type,应该是阿里自定义的HTTP的头部,具体用法未知,推测是跟CDN相关。

20220331235023

HTTP2/3

由于HTTP2/3并没有更改原先的HTTP语义,而且其主要是从传输和传输层协议层面上进行优化,因此之前HTTP1.1的优化手段依然可以继续使用。但HTTP2由于新增了 服务器端推送的功能,在浏览器中存在Push Cache,用于 HTTP2 在 sever push 阶段存在的缓存。

参考文章

service-worker-caching-and-http-caching

cache模式

rfc7234

性能设计篇之“缓存”-极客时间

http-caching-cache-control-vary

浏览器之 HTTP 缓存机制解读