减少冗余的数据传输。多个页面中都展示了同一张图片。 缓解网络瓶颈。尤其是移动网络。 降低了 瞬间拥塞(Flash Crowds) 的可能。例如春运抢票,双十一剁手。 降低了距离时延。比如海南三亚的用户访问黑龙江哈尔滨的服务器(3441km),单光速(299792.458km/s)延迟就需要(23 ms)。
缓存无法保存每份文档,缓存的副本可能与服务器上的不一致。
用已有的缓存副本达到提供服务称为 缓存命中(cache hit),其他一些达到缓存的请求可能由于没有副本可用,而被转发给原始服务器。这称为 缓存未命中(cache miss)。由缓存提供服务的请求所占的比例称为缓存命中率 (cache hit rate)。
由于服务器的内容可能会发生变化。所以需要检测缓存的副本与服务器上的最新版本是否一致,这个行为称之为 HTTP 再验证(revalidation) 或 “新鲜度检测”
缓存对缓存的副本进行再验证时,会向服务器发送一个小的再验证请求验证副本是否变化,如果没变化,服务器会返回一个 304 响应;如果有变化则返回 200 响应, 然后 HTTP Agency 会请求新的副本;如过对象被删除了,服务器返回 404 响应,同时删除缓存。
验证缓存副本为有效时则称之未再 验证命中(revalidate hit) 或 缓慢命中(slow hit)
由缓存提供服务的请求所占比称为 缓存命中率 (cache hit rate)。
对于大型文档(高清视频,高保真音频等),由于文档本身的尺寸的问题,虽然访问的次数少,但整个的数据流量的贡献却更大,缓存命中率并不能表现出真实情况,此时,可以考虑采用 字节命中率(byte hit rate) 作为度量值。
私有缓存(private cache) 是单个用户专用的(例如,浏览器缓存)。 公有缓存(public cache) 是多个用户共享的,公有缓存是特殊的共享代理服务器,被称为 缓存代理服务器(caching proxy server) 或 代理缓存(proxy cache)。
代理缓存的 层次化(hierarchy) 的结构非常有意义,较小的缓存中未命中的请求会被较大的 父缓存(parent cache) 提供服务。
其基本思想就是在靠近客户端的地方使用小型廉价缓存,然后逐步采用更大,更强的缓存来装载更多的用户共享文档。
但是,每一个代理拦截都会增加性能损耗,当链路达到一定长度的时候,这种性能损耗会变得更加明显。
网状缓存(cache mesh) 中的代理缓存之间通过某种策略做出动态的缓存通信决策,决定与哪个父缓存进行对话,或彻底绕开缓存,直接链接原始服务器。这种代理缓存会决定选择何种路由对内容进行访问,管理或传送,因此称其为 内容路由器(content router)
一个内容路由器必须包含以下功能:
- 根据 URL 在父缓存或原始服务器之间进行动态选择
- 根据 URL 动态选择父缓存
- 前往父缓存之前,在本地搜索已缓存的副本
- 允许其它 对等(peer)实体 访问其缓存内容,不允许 Internet 流量访问。
可选的对等支持的缓存称为 兄弟缓存(sibling cache)。HTTP 本身不支持兄弟缓存,所以人们基于 HTTP 进行了扩展,例如 英特网缓存协议 (Iternet Cache Protocol, ICP) 和 超文本缓存协议(HyperText Caching Protocol, HTCP)。
- 接收 —— 接收请求报文
- 解析 —— 解析报文,提取出 URL 和各种首部放入易操作的数据结构中
- 查询 —— 检查是否有本地缓存,如果不存在本地缓存,就去获取一份副本
- 新鲜度检测 —— 查看副本是否是最新版本
- 创建响应 —— 缓存会用新的首部和已缓存的主题构建响应报文
- 发送 —— 通过网络响应发送给客户端
- 日志 —— 创建一个日志文件描述这个事务
(引用自《HTTP 权威指南》中文版)
HTTP 的响应中的首部 Expires
和 Cache-Control
表示文档的过期时间,如果缓存副本处于这个时间段内,则表示缓存副本未过期。
Expires
首部是的值是一个绝对时间,例如:
Expires: Fri, 05 Jul 2002, 05:00:00 GMT
Cache-Control
支持多种指令,例如
Cache-Control: public, max-age=31536000
指令 | 描述 | 请求 | 响应 |
---|---|---|---|
public |
响应可以被任何对象缓存 | NO | YES |
private |
响应只可被单个对象缓存 | NO | YES |
no-cache |
使用缓存前强制向服务器验证 | YES | YES |
no-store |
不允许缓存,并删除当前缓存副本 | YES | YES |
max-age=<seconds> |
设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。 | YES | YES |
s-maxage=<seconds> |
覆盖 max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。 | NO | YES |
max-stale[=<seconds>] |
表明客户端愿意接收一个已经过期的资源。 | YES | NO |
min-fresh=<seconds> |
表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。 | YES | NO |
stale-while-revalidate=<seconds> |
表明客户端愿意接受陈旧的响应,同时在后台异步检查新的响应。 | NO | NO |
stale-if-error=<seconds> |
表示如果新的检查失败,则客户愿意接受陈旧的响应。 | NO | NO |
must-revalidate |
一旦资源过期(比如已经超过 max-age ),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。 |
NO | YES |
proxy-revalidate |
与 must-revalidate 作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。 |
NO | YES |
immutable |
表示响应正文不会随时间而改变。资源(如果未过期),因此客户端不应发送重新验证请求头(例如 If-None-Match 或 If-Modified-Since )来检查更新,即使用户显式地刷新页面。 |
NO | NO |
no-transform |
不得对资源进行转换或转变。 | YES | YES |
only-if-cached |
表明客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝。 | YES | NO |
在
HTTP 1.1
及以上版本中,如果Cache-Control
的值为max-age=<seconds>
或s-maxage=<seconds>
则会忽略Expires
如果当前缓存副本不够新鲜,向服务器发起验证。一般通过以下 HTTP 首部上的值进行判断
If-None-Match
和Etag
(组合使用)If-Modified-Since
和Last-Modified
(组合使用)
服务器返回文档内容时,服务器根据一定的策略生成一个 Hash 值,同时在相应头上增加字段 Etag
,该字段的值为生成的 Hash 值。
当客服端对该文档进行再验证时,在请求头中增加首部 If-None-Match
,首部的值为保存副本的 Etag
的值。
服务器对请求首部中的 If-None-Match
值与文档最新版本的 Hash 值进行比较,如果值相同,则返回 304 相应,如果文档本删除返回 404 相应,如果不相同且没删除则返回 200。
客户端下载新版本的文档,并根据新的响应首部进行文档缓存的处理。
例如 Apache 服务器根据文档的 索引节(INode),大小(Size),和最后修改时间(MTime)进行 Hash 计算得到
Etag
的值
与 If-None-Match
和 Etag
不同的是,If-Modified-Since
和 Last-Modified
的值是一个具体的时间点
// HTTP Response
Last-Modified:Tue, 24 Feb 2009 08:01:04 GMT
// HTTP Request
If-Modified-Since:Tue, 24 Feb 2009 08:01:04 GMT
如果响应中没有 Cache-Control: max-age<Seconds>
也没有 Expires
首部,缓存可以计算出一个最大试探性周期。
LM-Factor
是一种常见的试探性过期算法。其根据最后修改日期来估计文档有多易变。
缓存通常会为新的文档设置一个默认的新鲜周期,通常是一个小时或一天,有时比较保守的会设置为 0, 有些激进的会设置为一周。
TL;DR:
- 在资源“过期”之前,将一直使用本地缓存的响应。
- 您可以通过在网址中嵌入文件内容指纹,强制客户端更新到新版本的响应。
- 为获得最佳性能,每个应用都需要定义自己的缓存层次结构。
不过,如果您想更新或废弃缓存的响应,该怎么办?例如,假定您已告诉访问者将某个 CSS 样式表缓存长达 24 小时 (max-age=86400),但设计人员刚刚提交了一个您希望所有用户都能使用的更新。 您该如何通知拥有现在“已过时”的 CSS 缓存副本的所有访问者更新其缓存?在不更改资源网址的情况下,您做不到。(shopee interview question)
- 使用一致的网址:如果您在不同的网址上提供相同的内容,将会多次提取和存储这些内容。 网址区分大小写。特别是 PWA 需要注意该问题 确保服务器提供验证令牌 (ETag):有了验证令牌,当服务器上的资源未发生变化时,就不需要传送相同的字节。
- 确定中间缓存可以缓存哪些资源:对所有用户的响应完全相同的资源非常适合由 CDN 以及其他中间缓存进行缓存。
- 为每个资源确定最佳缓存周期:不同的资源可能有不同的更新要求。 为每个资源审核并确定合适的 max-age。
- 确定最适合您的网站的缓存层次结构:您可以通过为 HTML 文档组合使用包含内容指纹的资源网址和短时间或 no-cache 周期,来控制客户端获取更新的速度。
- 最大限度减少搅动:某些资源的更新比其他资源频繁。 如果资源的特定部分(例如 JavaScript 函数或 CSS 样式集)会经常更新,可以考虑将其代码作为单独的文件提供。 这样一来,每次提取更新时,其余内容(例如变化不是很频繁的内容库代码)可以从缓存提取,从而最大限度减少下载的内容大小。