本文翻译自: https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching ,主要用于个人记录和共享,若有疏漏错误,请不吝指正,谢谢!

通过重用已获取的资源,可大幅提高web站点和应用的性能。由于web缓存减少了延迟和网络流量,因此缩短了展示一个资源所需的时间。通过使用HTTP缓存机制,web站点可实现更快更灵活的响应。

不同类型的缓存

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。当发起一个请求时,web缓存会判断是否已有此请求的一个副本(之前已经请求过一次并且被缓存了),若有,则缓存会拦截此请求,并直接返回缓存中的请求结果副本,从而防止重新到源服务器下载资源。缓存的目的:减轻服务器压力(服务器不用每次为所有客户端提供服务了),提高访问效率(因为缓存离客户端最近,可直接提供资源副本,也可节省很多传输时间)。对于网站来讲,缓存是建设高性能网站的最重要的组件,但在另外方面,缓存必须进行合理的配置才能达到最佳效果,因为并非所有资源都是永久不变动的,所以我们需要保证某个资源的缓存仅在它未变动时有效。

所有不同类型的缓存,大致可以归为两类:私有缓存 和 共享缓存。共享缓存中存储的资源副本是供所有用户使用的(比如不同浏览器,不同机器),而私有缓存是仅提供给单个用户的专有缓存(不同用户保留不同私有缓存副本)。本文仅讨论浏览器缓存和代理缓存,但就目前来讲,还有很多其他类型的缓存,比如:网关缓存、CDN、反向代理缓存、负载均衡(负载均衡是部署在服务器端的,为多个web服务器提供更可靠、更高性能以及更易进行规模化扩展的方案)。

浏览器(私有)缓存

私有缓存是单个用户的专有缓存,一般来讲,在你的浏览器设置中就可以看到“缓存”的选项。浏览器缓存保留了用户通过HTTP下载的所有文档资源,前进/后退、保存、查看源代码等操作都可以使用到此缓存,而不用再重新访问服务器。同样的,有了缓存,我们还可以实现脱机浏览文档和资源。

代理(共享)缓存

共享缓存中存放的访问结果是提供给多个用户使用的。比如:ISP或你的公司可能会组建一个本地网络的代理,该代理(服务器)会缓存不同用户访问外网时请求的公共资源,这些公共资源被缓存后,下次其他用户也访问同一资源时,就会重用此已被缓存的资源(就不用再向源站获取了),从而减少了网络浏览和延迟。

缓存操作的目标

HTTP缓存虽然是可选的,但一般是所有人都需要的。HTTP缓存通常只缓存GET请求(其他请求一般不缓存),缓存的主键由请求方法和目标URI(通常只用到URI,因为一般仅缓存GET请求)组成。通常的缓存条目有:

  • 成功的查询请求的结果数据:状态码为200的GET响应(结果中可能包含资源数据如:HTML文档、图片或文件等)
  • 永久性跳转:状态码为301(Moved Permanently)的响应
  • 返回出错,文档不存在:状态码为404(Not Found)的响应
  • 不完整的结果数据:状态码为206(Partial Content)的响应(通过Range头发起的请求所返回的结果,Range用于只获取文档某一部分)
  • 其他非GET请求的结果(如果这些结果比较适合作为缓存的话)

缓存的条目也可能缓存多个,其中根据内容协商方式,每一个对应的二级键(header头中的字段)不一样。详见 Vary 头。

缓存控制

Cache-control头部

HTTP/1.1中,Cache-Control头用于指定缓存机制中的不同指令,它是可用在请求报文及响应报文中的通用头部。通过该头部提供的不同指令,你可以定义一个自己的缓存策略。

禁止缓存 方式

如下头部定义,在该方式下,缓存不会保存任何的客户端请求和服务器响应。每次客户端的请求都会发送到源服务器,并且每次源服务器返回的数据都会全部下载到客户端。

Cache-Control: no-store
Cache-Control: no-cache, no-store, must-revalidate

强制确认缓存 方式

如下头部定义,此方式下,每次有请求发出时,缓存会将此请求发到服务器(译者注:该请求应该会带有与本地缓存相关的验证字段),服务器端会验证请求中所描述的缓存是否过期,若未过期(译者注:实际就是返回304),则缓存才使用本地缓存副本。

Cache-Control: no-cache

私有和公共缓存

"public" 指令表示该响应可以被任何中间人(译者注:比如中间代理、CDN等)缓存。若指定了"public",则一些通常不被中间人缓存的页面(译者注:因为默认是private)(比如 带有HTTP验证信息(帐号密码)的页面 或 某些特定影响状态码的页面),将会被其缓存。

而 "private" 则表示该响应是专用于某单个用户的,中间人不能缓存此响应,该响应只能应用于浏览器私有缓存中。

Cache-Control: private
Cache-Control: public

过期机制

过期机制中,最重要的指令是 "max-age=<seconds>",表示资源能够被缓存(保持新鲜)的最大时间。与 Expires指令不同,该指令的值是相对于请求的那个时间之后的秒数。对于那些不会变动的文档资源,你可以直接将其设置为永久缓存,比如像图片、CSS文件、JS文件这些静态资源。

更多信息,参见下方的 Freshness 。

Cache-Control: max-age=31536000

验证确认

当使用了 "must-revalidate" 指令,那就意味着缓存在考虑使用一个陈旧的资源时,必须先验证它的状态,并且,已过期的缓存将不被使用。更多信息,参见下方的 Validation

Cache-Control: must-revalidate

Pragma 头部

Pragma 是HTTP/1.0规范中的头部,它已经不是可靠的用于过期控制的头部了,尽管它的行为和 Cache-Control: no-cache 一致(未设置Cache-Control头部的情况下)。Pragma 现仅用于兼容 HTTP/1.0 客户端。

新鲜度

理论上来讲,当一个资源被缓存存储后,该资源应该可以被永久存储在缓存中。由于缓存只有有限的空间用于存储资源副本,所以缓存会定期地将一些副本删除,这个过程叫做 缓存驱逐。另一方面,当服务器上面的资源进行了更新,那么缓存中的对应资源也应该被更新,由于HTTP是C/S模式的协议,服务器更新一个资源时,不可能直接通知客户端及其缓存,所以双方必须为该资源约定一个过期时间,在该过期时间之前,该资源(缓存副本)就是 新鲜的,当过了过期时间后,该资源(缓存副本)则变为 陈旧的。驱逐算法用于将陈旧的资源(缓存副本)替换为新鲜的,注意,一个陈旧的资源(缓存副本)是不会直接被清除或忽略的,当客户端发起一个请求时,缓存检索到已有一个对应的陈旧资源(缓存副本),则缓存会先将此请求附加一个If-None-Match头,然后发给目标服务器,以此来检查该资源副本是否是依然还是算新鲜的,若服务器返回了 304 (Not Modified)(该响应不会有带有实体信息),则表示此资源副本是新鲜的,这样一来,可以节省一些带宽。(译者注:若服务器通过 If-None-Match 或 If-Modified-Since判断后发现已过期,那么会带有该资源的实体内容返回)

下面是一个代理共享缓存的过程示例:

新鲜度的生命周期是通过若干头部值来计算的,如果设置了 "Cache-control: max-age=N" 头部,那么新鲜度的生命期则等于 N。经常情况下,可能未设置此头部,则会检查 Expires 头部是否存在,若 Expires 头部存在,则新鲜度生命期 等于 该头部的值 减去 Date 头部的值。若两种头部都未设置,则会查找 Last-Modified 头部,若存在,则新鲜度生命期 等于 Date 头部值 减去 Last-modified 头部值 再除以 10。

expirationTime = responseTime + freshnessLifetime - currentAge

上式中,responseTime 表示浏览器接收到此响应的那个时间点。

资源版本化

缓存使用越频繁(译者注:跟命中率有关了),那么网站的响应速度和效率就越高,为此,在最佳实践中,我们推荐尽可能地将过期时间设置得长一些,但这会导致我们很难去更新那些不常变动的资源。比如我们经常会遇到这样的需求:很多页面都引用了一些JS和CSS文件,当这些文件的内容变动时,我们希望能尽快地让其在缓存中更新。

web开发者们研究出一个方案,Steve Sounders称它为 revving[1]。其原理是,将那些经常更新的文件的文件名通过一种特别的方式来命名,即文件名中加入版本号,这样一来,每一次文件内容改变,文件名也被一起改变,就相当于新建了另一个不同的资源,那我们就可以将该资源设置为永久不过期了(通常设置为1年以上)。但为了引用这个改动后的资源,所以链接到此资源的链接地址都需要改变(译者注:其文件名改变后,相对应的URI也改变,所以链接到此资源的地址也应该改变),这也是该方案的缺点:带来了额外的复杂性,通常web开发者会使用一些工具来自动应对此缺点(译者注:如webpack),当不常变动的资源改变时,这些资源的文件名URI也随之改变,而引用这些资源的另外的经常变动的资源,也将随之改变(引用地址改变),当客户端请求常变动的资源时,它里面引用的不常变动的资源,由于加入了版本号,所以其新的版本也被下载。

这种方案有一个好处:可以解决2个资源过期时间不一致导致不一同被更新的问题。这在当网站的CSS或JS资源拥有共同的依赖时显得尤为重要,比如他们引用了同一个HTML元素,导致他们互相依赖。(译者注:举例,比如当前缓存了两个JS文件A,B,A里面是比较早期的功能,A先过期,B是晚于A开发的功能,且依赖于A,B后过期,假如这期间服务器端A和B都做了改变,加了新的功能,那么当A过期时拿到了最新版的A,而B还未过期,则使用的是旧版的B,这样页面运行时,可能导致严重错误)。

添加在资源名称上面的版本号并非必须是像1.1.3这样的常用版本定义方式,甚至可以仅仅是一个连续递增的数字,只要版本号不冲突,它可以是任意的,比如hash值或日期时间。

缓存的验证确认

当用户点击刷新(重新加载)按钮时,就会触发一个再次验证确认,当用户正常访问某网站/资源时,浏览器会检查该请求对应的缓存内容,若之前缓存的响应内容中,包含了 "Cache-control: must-revalidate" 头部,那么也会触发一个再次验证确认。另外一个引发再次验证确认的因素是:用户可以在浏览器的 Advanced->Cache 设置选项面板中设置 强制每次加载页面时都进行验证确认。

当一个缓存副本已经到了过期时间,那么就会先到服务器验证确认此缓存的新鲜度,或者直接从服务器获取该资源最新的内容。验证确认操作仅仅在服务器的响应信息中提供了 强验证器弱验证器 才会发生。

ETag头部

响应报文中的 ETag 头是一个对 用户代理透明 的值,被用来作为强验证器,意思就是说,像浏览器这样的用户代理程序并不知道其值代表的含义。若在服务器的响应报文中含有 ETag 头部,则后续客户端发起同一请求时,会附加一个 If-None-Match 头部(其值为之前Etag的值)用于让服务器验证该请求对应的缓存是否新鲜。

响应报文中的 Last-Modified 头用来作为弱验证器,之所以作为“弱”,是因为它最多只能精确到秒,若服务器的响应报文中存在 Last-Modified 头部,那么客户端发起一个 If-Modified-Since 请求来验证缓存是否新鲜。

当发起了一个验证请求,服务器可以忽略此验证请求并返回一个正常的 200 OK 响应报文,或者也可以返回 304 Not Modified(带有一个空的实体)来告知浏览器:你可以使用当前缓存副本,后者(304)的响应报文中也可以附加一些头部,用来更新当前缓存副本中缓存的头部信息。

可变响应 - Vary头部

响应报文中的 Vary 头部用于 决定如何匹配后续请求的头部,从而决定是否采用某缓存副本,而不是从服务器获得一个新的副本。

当发起一个请求时,缓存命中了一个副本(之前缓存的响应报文信息),而这个副本中含有 Vary 头部,那么缓存需要检查 Vary 头部中所列出的头部字段,若在当前请求中的这些头部字段值 与 缓存副本中响应的头部字段值匹配,那么可以使用此缓存副本,否则需要不能使用此副本(重新请求服务器)。

有了这个头部,就可以动态地提供内容了,举个例子,当我们使用 Vary: User-Agent 头部时,缓存服务器就需要去查看新发起的请求的 User-Agent 头部是否匹配,然后再决定是否采用缓存。如果你需要为手机端用户展示不同的内容,此举可以防止将电脑端的缓存错误地提供给了手机端的用户,并且还可以帮助Google这些搜索引擎发现并抓取手机端版本的页面,以及告诉搜索引擎这不是 Cloaking 作弊。

Vary: User-Agent

由于 User-Agent 头部在移动端和电脑端中的值都是不同的,所以缓存就不会错误地将移动端内容提供给电脑端用户了,反之亦然。

参考资料

HTTP缓存机制[译文]的更多相关文章

  1. 【腾讯Bugly干货分享】彻底弄懂 Http 缓存机制 - 基于缓存策略三要素分解法

    本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/qOMO0LIdA47j3RjhbCWUEQ 作者:李 ...

  2. MyCat源码分析系列之——BufferPool与缓存机制

    更多MyCat源码分析,请戳MyCat源码分析系列 BufferPool MyCat的缓冲区采用的是java.nio.ByteBuffer,由BufferPool类统一管理,相关的设置在SystemC ...

  3. Java三大框架之——Hibernate中的三种数据持久状态和缓存机制

    Hibernate中的三种状态   瞬时状态:刚创建的对象还没有被Session持久化.缓存中不存在这个对象的数据并且数据库中没有这个对象对应的数据为瞬时状态这个时候是没有OID. 持久状态:对象经过 ...

  4. Spring缓存机制的理解

    在spring缓存机制中,包括了两个方面的缓存操作:1.缓存某个方法返回的结果:2.在某个方法执行前或后清空缓存. 下面写两个类来模拟Spring的缓存机制: package com.sin90lzc ...

  5. hibernate缓存机制(转)

    原文出处:http://www.cnblogs.com/wean/archive/2012/05/16/2502724.html 一.why(为什么要用Hibernate缓存?) Hibernate是 ...

  6. [转]Android ListView 与 RecyclerView 对比浅析—缓存机制

    从源码角度剖析ListView 与 RecyclerView 缓存机制的不同 https://zhuanlan.zhihu.com/p/23339185 原文地址:http://dev.qq.com/ ...

  7. HTML5应用缓存机制

    首先先上一张图: 用360浏览器的用户对这张图应该都是耳熟能详了吧,没错,当网络不通畅时使用360浏览器,便会有这张图弹出来.为什么没有网络还能弹出这一副画面呢?这就关乎HTML5的应用缓存机制了. ...

  8. 从零开始,搭建博客系统MVC5+EF6搭建框架(3),添加Nlog日志、缓存机制(MemoryCache、RedisCache)、创建控制器父类BaseController

    一.回顾系统进度以及本章概要 目前博客系统已经数据库创建.以及依赖注入Autofac集成,接下来就是日志和缓存集成,这里日志用的是Nlog,其实还有其他的日志框架如log4,这些博客园都有很多介绍,这 ...

  9. C#无限极分类树-创建-排序-读取 用Asp.Net Core+EF实现之方法二:加入缓存机制

    在上一篇文章中我用递归方法实现了管理菜单,在上一节我也提到要考虑用缓存,也算是学习一下.Net Core的缓存机制. 关于.Net Core的缓存,官方有三种实现: 1.In Memory Cachi ...

随机推荐

  1. HDU2058

    The sum problem Time Limit: 5000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) ...

  2. LeetCode题目答案索引

    LeetCode-Two Sum LeetCode-Median of Two Sorted Arrays LeetCode-Longest Substring Without Repeating C ...

  3. php 中的$argv与$argc

    例如 php test.php as a joke print_r($argv); echo $argc; print_r($argv); array_shift($argv); echo $argc ...

  4. ruby+rt标签的效果

    代码如下: <ruby>我是孤行者<rt>wo shi gu xing zhe</tr></ruby> 效果如下

  5. HDU 2896 AC自动机 裸题

    中文题题意不再赘述 注意字符范围是可见字符,从32开始到95 char c - 32 #include <stdio.h> #include <string.h> #inclu ...

  6. [转] 在 Linux 中怎样使用cp命令合并目录树

    PS:通过cp -r --link a/* b/* merged 硬链接不需要复制 怎样将两个布局相似的目录树合并成一个新的目录树?为理解该问题让我们思考下面的例子. 假设 dir1 和 dir2 目 ...

  7. js中的隐式转换

    js中的不同的数据类型之间的比较转换规则如下: 1. 对象和布尔值比较 对象和布尔值进行比较时,对象先转换为字符串,然后再转换为数字,布尔值直接转换为数字 [] == true; //false [] ...

  8. MyEclipse修改servlet模版

    找到myeclipse安装目录中的 然后把这个jar包复制到桌面 以压缩包的方式打开 之后保存, 然后把修改的这个jar包放到刚开的路径,替换已经存在的! 完成!

  9. JavaScript 浮点数运算 精度问题

    JavaScript小数在做四则运算时,精度会丢失,这会在项目中引起诸多不便,先请看下面脚本. //加减 <script type="text/javascript" lan ...

  10. Html.RenderPartial与Html.RenderAction区别(转)

    Html.RenderPartial与Html.RenderAction这两个方法都是用来在界面上嵌入用户控件的. Html.RenderPartial是直接将用户控件嵌入到界面上: <%Htm ...