1、概论
1.1、 什么是缓存?
Web缓存是可以自动保存常见文档副本的HTTP设备。当Web请求抵达缓存时,如果本地有“已缓存的”副本,就可以从本地存储设备而不是原始服务器中提取这个文档。因此,可以这样理解,缓存拦截了客户端的请求,代替服务器端做出响应。
1.2、缓存的作用
使用缓存主要有如下几个有点:
- 缓存减少了冗余的数据传输,节省了你的网络费用。
- 缓存缓解了网络瓶颈问题。不需要更多的带宽就能够更快的加载页面。
- 缓存降低了对原始服务器的要求。服务器可以更快地响应,避免过载的出现。
- 缓存降低了距离时延,因为从较远的地方加载页面会更慢一些。
1.3、缓存分类
缓存主要分为两类:
- 私有缓存:缓存存储在客户端的存储器里面。
- 公有代理缓存:又称为缓存代理服务器或者代理缓存。它较私有缓存的优点是,避免了每个客户端都需要去服务器获取数据,从而节省一定的流量。
二者之间的对比如下图所示:
2、缓存的处理步骤
一个缓存从接收到请求到做出响应,大概可以分为如下7个步骤:
(1)接收——缓存从网络中读取抵达的请求报文。
(2)解析——缓存对报文进行解析,提取出URL和各种首部。
(3)查询——缓存查看是否有本地副本可用,如果没有,就获取一份副本(并将其保存在本地)。
(4)新鲜度检测——缓存查看已缓存副本是否足够新鲜,如果不是,就询问服务器是否有任何更新。
(5)创建响应——缓存会用新的首部和已缓存的主体来构建一条响应报文。
(6)发送——缓存通过网络将响应发回给客户端。
(7)日志——缓存可选地创建一个日志文件条目来描述这个事务。
下图描述了缓存中有新鲜缓存时,缓存的处理过程。由此可以看出,这整个过程已经与服务器没有关系了:
2.1、缓存处理流程图
下图以简化的形式显示了缓存是如何处理请求以获取一个方法为GET的URL的。
3、判断是否新鲜
从上一节的流程图可以看出,有两个工作在缓存的过程中是非常重要的。一个是如何判断该缓存还是新鲜的?另一个是验证缓存副本是否与服务器原本一致?这两个工作都是由HTTP报文相关的首部字段决定的。这一节我们先考虑第一个问题,下一节再探讨第二个问题。
3.1、Expires
服务器使用Expires首部来指定过期日期。比如说,如果服务器发送一个响应报文,其中首部有如下字段:
Expires: Mon, 30 Oct 2017, 12:00:00 GMT
这也就说明,该报文会在2017年10月30日的12点整过期。在此后到来的响应请求,缓存就会判断其中的副本不新鲜,需要与服务器进行再验证。
3.2、Cache-Control: max-age
使用Expires来判断是否新鲜虽然可行,但也有一定的问题。那就是它所依赖的是缓存本地的时间,假设我所用的缓存是私人缓存,我将自己电脑的时间调到2200年,那么,该副本将在我的有生之年都不会过期。待我老时,浏览的还是几十年前的页面。
实际上Expires是HTTP 1.0协议引入的。在HTTP 1.1协议中引入了Cache-Control: max-age头部,解决了Expires所遇到的难题。Expires使用的是绝对时间,而max-age使用的则是相对时间。当服务器生产一个响应或者再验证成功时,它会在报文中存储此刻的时间,max-age中的时间就是相对该时间的。比如,如果响应报文是此刻生产的,那么下面的语句就是说该缓存会在60秒后过期,如果你60秒后再来访问该URL,那么就需要与服务器进行再验证:
Cache-Control: max-age=60
3.3、请求报文如何控制新鲜度
上面的内容都是针对服务器的响应报文的。那么,请求报文有没有办法控制新鲜度呢?或者可以先问这样一个问题,客户端是否有必要插手管理缓存的新鲜度呢?
答案是肯定,比如服务器端使用了Expires字段,而我一不小心将时间设置成了2200年。一天又一天,无论我怎么请求,该页面亘古不变。我依然没想到是时间设置的错误(我可真笨)。然而,有一天,我不再只是在浏览器输入网址然后敲回车键,而是按F5刷新,页面终于奇迹的刷新了。
请求报文中与缓存新鲜度有关的也在名为Cache-Control的首部字段中。大概有如下几种:
3.3.1 max-age
和响应报文中的max-age类似,该值也是一个相对时间。如果当前时间减去缓存中存储的响应报文的创建时间超过max-age的值的话,那么缓存就得去和服务器再验证了。
3.3.2 max-stale
stale的意思是不新鲜,max-stale的意思就是说,在多大的范围内,客户端还是愿意接受缓存中保存的不新鲜的副本的。也即是说,如果当前时间超过了max-age的范围,但却没有超过max-stale值得范围,那么缓存副本还是可以代替服务器做出响应的。
3.3.3、min-fresh
该字段的意思是说,客户端不希望缓存中副本的新鲜生命周期不低于其创建时间再加上min-fresh指定的时间。如果超过了,那就从服务器获取。
4、再验证
第二个工作就是,缓存是如何与服务器进行交互并再验证的呢?HTTP协议提供了两种主要的方式:If-Modified-Since:Date和If-None-Match:Etag。它们又被称为条件验证。这两个标签的原理类似,只是有一点点细微的差别而已。
整个再验证的过程和客户端没有任何关系,仅涉缓存和服务器双方。
- 对于If-Modified-Since:Date的方式,缓存会根据缓存副本的创建时间来创建If-Modified-Since的值,这个值可以和服务器的响应首部Last-Modified相配合使用,以此判断内容是否被修改了。
- 如果使用的是If-None-Match:Etag方式,那么服务器会为该文档提供一个特殊的标签(Etag),当缓存去和服务器沟通时,发现服务器原本的Etag和缓存副本不同,也就认为服务器中的资源被修改了。
如果再验证发现缓存副本没有更改,那么服务器会回送给客户端304 Not Modified响应。客户端自动重定向到缓存拉取副本,同时缓存更新相关首部。如Age、max-age、Etag等。
如果再验证发现缓存副本已改变,那么服务器就会将携带新首部的新文档发送给缓存,缓存进行存取后并对首部进行改装后再以200 OK对客户端做出响应。
如果再验证发现服务器中已没有此资源时,服务器直接向客户端返回404 Not Found响应。
注:如果报文中即有If-Modified-Since验证,又有If-None-Match验证,那么只有当两个条件都满足的情况下才会返回304 Not Modified*响应。
5、缓存控制
到这里为止,缓存的处理流程基本上已经理清了。然而,HTTP协议的能力不仅如此,它还为我们开发人员提供了强大的控制缓存的机制,我们可以通过相关首部控制是否使用缓存,是否一定要再验证等,这也是由名为Cache-Control的首部字段提供的:
5.1、no-Store
如果请求报文或响应报文中含有no-Store时,它会禁止缓存对响应进行复制。
5.2、no-Cache
如果请求报文和响应报文中含有no-Cache时,也就是强制了缓存需要与服务器端进行在验证。
5.3、must-revalidate
这是响应报文专用的。
上面说过,请求报文中可以包含Cache-Control:max-stale标签,它允许缓存提供一定期限的超过了新鲜生命周期的信息。而如果响应报文使用了must-revalidate,那就意味着,缓存将严格遵守过期信息,只要超过了新鲜生命周期,就需要和服务器进行再验证。
5.4、only-if-cached
这是请求报文专用的。
如果请求报文中有此标签。那就意味着,客户端只希望从缓存中读取资源。如果缓存中不存在,缓存会返回504 Gateway Timeout响应。
5.5、控制私有和公有
- 如果响应报文中含有Cache-Control:public字段,那就意味着,服务器只希望代理缓存对其进行复制。
- 反之,如果响应报文中含有Cache-Control:private字段,那就意味着,服务器只希望私有缓存对其进行复制。
5.6 补充:Pragma
由于Control-Cache是HTTP 1.1协议指定的,因此,为了兼容1.0版本,客户端可以发送包含Pragma:no-Cache首部字段的请求报文。如果其中有Control-Cache:no-Cache并且该字段能够被理解,那么Pragma中定义的就会被忽略。
6、一种缓存策略决策树
作为Web服务器端的开发者,可以参考google提供如下决策树:
7、总结
缓存机制是HTTP协议中相对较难的一块,也是非常重要的一块,因此,很难说这篇文章没有任何错误,如有错误还请指正批评,谢谢!
参考资料
《HTTP权威指南》第七章
Google Developers HTTP Cache
rfc7234