重新学习HTTP缓存

2019-08-14

HTTP缓存是什么

缓存是计算机科学中非常常见的概念,在HTTP协议中,缓存指:

当发起一个资源的请求时,如果本地存储设备有资源“已缓存”的副本,就直接从本地提取这个文档;如果本地存储设备没有该资源,再去请求原始服务器,并将请求到的结果保存下来等待下次缓存使用。

那么,缓存是基于这样一个假设:在一段时间内,某些资源是不会改变的。

缓存的优点大致有:

  • 缓存减少了冗余的数据传输,节省了你的网络费用。
  • 缓存缓解了网络瓶颈的问题。不需要更多的带宽就能够更快地加载页面。
  • 缓存降低了对原始服务器的要求。服务器可以更快地响应,避免过载的出现。
  • 缓存降低了距离时延,因为从较远的地方加载页面会更慢一些。

缓存什么资源

假设我们不知道现在的HTTP缓存策略,就从头开始考虑怎么缓存资源。

首先考虑什么类型的资源可以被缓存?在HTTP方法的语义上来看,应该是幂等的无副作用的方法请求的资源可以被缓存。很好理解,缓存就是假设一定时间内资源不会改变,如果方法不是幂等,有副作用的,每次请求出来的资源都不一样,也无从谈起缓存了。所以,对应到HTTP方法中,应该就是GET方法。

所以,包括浏览器直接url访问、html中link标签引用的资源、ajax调用的GET方法等触发GET的场景,都是可以配置缓存的。对于其他方法,是不能缓存的。

“新鲜度”问题

在定义缓存行为时,我们说需要去检查本地存储设备有资源“已缓存”的资源。如何检查资源是已缓存的呢?这就是“新鲜度”问题,也称为缓存策略。我们称缓存有效时是“新鲜的”

同时,因为HTTP是无状态的,不能要求服务器去记录客户端哪些缓存,需要让客户端拥有一定的判断能力。

现代HTTP中,有两个简单的策略判断缓存“新鲜度”:文档过期(expiration)和服务器再验证(revalidation),在前端领域常常被称为强缓存(强制缓存)和协商缓存。

强缓存

强缓存即设置一个资源过期时间,检查资源是否过期,如果未过期,就直接视为缓存是新鲜的,就像在超市买牛奶看保质期一样,非常的简单。

在具体HTTP协议中有两个头字段实现此功能:

header 描述
Cache-Control:max-age max-age 值定义了文档的最大使用期——从第一次生成文档到文档不再新鲜、无法使用为止,最大的合法生存时间(以秒为单位)
Cache-Control: max-age=484200
Expires 指定一个绝对的过期日期。如果过期日期已经过了,就说明文档不再新鲜了
Expires: Fri, 05 Jul 2002, 05:00:00 GMT

此外Cache-Control还可以进行更细粒度的缓存控制,比如

  • 控制HTTP结构中的其他节点是否对资源进行缓存(如代理服务器、隧道服务器等)

    Cache-Control: public // 可以被其他HTTP节点缓存
    Cache-Control: private // 只能被客户端缓存
    
  • 是否允许使用过期缓存提供资源,比如当服务器不可用时

    Cache-Control: must-revalidate // 就算服务器不可用,也不可提供过期资源
    Cache-Control: max-statle [=<s>] // 可以提供过期资源,可以指定规则有效的秒数
    
  • 完全控制客户端完全不进行任何缓存行为

    Cache-Control: no-cache
    

协商缓存

协商缓存即资源可能已经过了过期时间,或者没设置过期时间,但是实际上服务器资源并没有更新,客户端可以使用缓存这样的场景。对于客户端,其信息是缺失的,需要和服务器进行协商才知道是否能够用缓存。

协商缓存不依赖过期时间,需要客户端和服务器用资源进行协商,那么就需要对资源进行唯一标志,客户端拿着资源的标志去问服务端,我这个编号的资源是否过期,按照服务器的响应来决定是否用缓存,若协商缓存成功,服务器返回304,客户端就可以拿缓存进行响应了。

在具体的HTTP协议中有两个头字段实现此功能:

header 描述
If-Modified-Since:<date> 如果从指定日期之后文档被修改过了,就执行请求的方法。可以与Last-Modified服务器响应首部配合使用,只有在内容被修改后与已缓存版本有所不同的时候才去获取内容
If-None-Match:<tags> 服务器可以为文档提供特殊的标签(参见ETag),而不是将其与最近修改日期相匹配,这些标签就像序列号一样。如果已缓存标签与服务器文档中的标签有所不同,If-None-Match 首部就会执行所请求的方法

从描述可以看出,这两个首部的标志策略不一样:

  • If-Modified-Since是对资源用修改时间进行唯一标志,服务端需要在提供资源时附带修改时间Last-Modified。但完全依赖修改时间的标志有些天然的缺陷:
    • 某些文档可能被操作,更改了最后修改时间(比如linux的touch指令),但实际包含的数据往往是一样的。
    • 某些修改可能并不重要,不需要让所有的缓存数据都失效。
  • If-None-Match不再依赖时间,是通过一套id的机制(实体标签ETag)来完成对资源的唯一标志,服务端需要在提供资源时带上资源的ETag,由服务端自己编程实现什么时候缓存应该更新(更新资源的ETag)

工作流程

一般来说,缓存的工作流程可以概括为:

  1. 接收——缓存从网络中读取抵达的请求报文。
  2. 解析——缓存对报文进行解析,提取出 URL 和各种首部。
  3. 查询——缓存查看是否有本地副本可用,如果有,就获取一份副本。
  4. 新鲜度检测——缓存查看已缓存副本是否足够新鲜(强缓存);如果不是,就询问服务器是否有任何更新(协商缓存);
  5. 创建响应——缓存会用新的首部和已缓存的主体来构建一条响应报文。
  6. 发送——缓存通过网络将响应发回给客户端。