写在前面
随着项目的推进,渐渐的要上https,要从tomcat服务器转换到nginx+tomcat组合的方式了。 nginx有个问题就是缓存太”严重”,我认为这其实是一个大的优化,如果用好了可以对项目体验起到很好的催化,这也是nginx作为服务器的前端的意义所在。 但是往往当开发人员对http理解薄弱的,就会对此很搓火。因为过强的缓存会导致页面滞后,debug时候很多东西无法及时出来,只能动不动强制清缓存,大大破坏了开发体验。
由来
其实一直一来都在做强缓存。新起的项目都做好了资源的hash。但是如果不是这次错配的nginx导致个人寻根结底找原因,也不会有这篇关于http缓存的文章。 HTTP的文章其实网上已经有很多了。个人完全不认为会比那些无比详细和成系列的雄文对比。仅仅是个人对知识体系的一个总结吧。
言归正传。问题的由来其实是因为nginx的响应头配置有问题,它里面既没有 Cache-Control,也没有Expires。导致很多资源无端200(from cache)而不304,使得资源存在更新不及时的问题.
举个栗子,这是清空缓存后第一次访问资源的http响应头:
1 | HTTP/1.1 200 OK |
这是对应的请求头:
1 | Accept:image/webp,image/*,*/*;q=0.8 |
然后在地址栏敲回车以后以后,返回的200(from cache),而我不是猜想的304 总之一句话就是这个问题的关键在于:响应头里既没有 Cache-Control也没有Expires的时候,是如何在新鲜度检测阶段直接认为新鲜度没有问题的。
问题的抛出
在搞清整个问题之前我觉得还是要将HTTP缓存机制好好整理处理作为问题解决的前提和疑问抛出的前提。
用最简单的话来说,服务器缓存启用是存在两个判断机制的:
- 新鲜度检测阶段 需要依赖响应头的Cache-Control和Expires,通过才会返回200(from cache)
- 资源二次校验阶段 校验资源一致性,如果一致返回304,如果不一致返回200,当然,如果没有找到返回404
现在的问题就抛出来了,为什么,响应头里面没有Cache-Control和Expires,那么按照上诉的缓存启用机制,返回的状态吗可能是304,可能是200,可能是404或者更多的,但是就是不可能是200(from cache)。但是现实用惨淡的现实告诉我们,结果就是200(from cache)!这是为什么呢?
HTTP缓存
用户操作对缓存的影响
用户不同的操作是对缓存存在固有影响,正常链接点入、新tab打开网址/窗口、F5刷新、前进后退、Ctrl+F5强刷,对缓存都会不同的表现
用户操作 | Exprires/Cache-Control | Last-Modified/Etag |
---|---|---|
地址栏回车 | 有效 | 有效 |
页面链接转跳 | 有效 | 有效 |
新开窗口 | 有效 | 有效 |
前进后退 | 无效 | 有效 |
Ctrl+F5强刷 | 无效 | 无效 |
缓存机制
缓存总体上的流程是这样的(原图来自《http权威指南》,个人有涂鸦):
新鲜度检测
Cache-Control与Expires 是新鲜度检测的关键:
Cache-Control与Expires的作用一致,都是指明当前资源的有效期。只不过Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于Expires。
详细一点说明的话,Expires是http/1.0定义的资源有效期,它是一个历史遗留产物。它的值是一个绝对值,它显示指定了一个日期作为过期时间。然而经过一段时间实践之后,Bug就出来了:当客户端可服务器时间不一致,那么到底怎么处理呢?
Cache-Control是http/1.1定义的,功能很多,但是核心的我认为还是还是改变了资源有效期的表达方式,我认为最大的意义是Cache-Control是Expires的bugfix产物(作为bugfix优先级当然会更高)。它使用max-age来根据最后时间加上这个max-age动态计算了过期时间,这样就不存在Expires的Bug了。
Cache-Control可选值:(偷懒摘自《翻译:web制作、开发人员需知的Web缓存知》)
- **max-age=[秒]**:表示在这个时间范围内缓存是新鲜的无需更新。类似Expires时间,不过这个时间是相对的,而不是绝对的。也就是某次请求成功后多少秒内缓存是新鲜的。
- **s-maxage=[秒]**:类似max-age, 除了仅应用于共享缓存(如代理)。
- public:标记认证的响应才能够被缓存。一般而言,需要认证HTTP请求内容会自动私有化(不会被缓存Add)。
- private:允许缓存专门为某一个用户存储响应,比方说在浏览器中;共享缓存一般不会,例如在代理中。
- no-cache:每次在释放缓存副本之前都强制发送请求给源服务器进行验证,这在确保认证有效性上很管用(和public结合使用)或者保证内容必须是即时的,不得无视缓存的所有优点,如国内的微博、twitter等的刷新显示Add。
- no-store:强制缓存在任何情况下都不要保留任何副本。
- must-revalidate:告诉缓存,我给你准备了一些关于新鲜度的信息,在表现的时候要严格遵循之。HTTP允许缓存在某些特定情况下返回过期数据,指定了这个属性,相对于告诉缓存,你丫必须严格遵循我的规则。
- proxy-revalidate:类似must-revalidate,除了只能应用于代理缓存。
个人注:理想的情况下,Cache-Control与Expires可以都标明以达到最优兼容,但是这不是必须都有的,多数可以只有其中一个也不会有什么大问题(如果引起BUG就另说了),但是——不要两个都没有,不然会引起相对诡异的现象,如果本文的例子。
——但是你硬要这么干,确实可以两个都没有.
资源二次校验
Last-Modified/ETag 这两个数据用于确定数据是否改变
Last-Modified嘛,很好理解,文件最后修改时间。如果这个东西那么好用的话其实Etag不过画蛇添足罢了,但是很可惜的Last-Modified的时间度量单位是秒——这意味着一秒内的修改服务器分辨不出来——股票行情系统和金融系统对此有利益攸关的需求。
所以就有了ETag,这个东西就是类似文件hash的东西,在服务器上用于分辨文件是不是同一个。
Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag。一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304
个人注:如果你想用304,Last-Modified和ETag是很重要的属性。如果你还想用304,那么就不要删除它们(删除其中一个会不会有影响个人还未验证)。
新鲜度校验的补充
试探性过期(本章摘自HTTP权威指南Page193)
如果响应中没有 Cache-Control: max-age 首部,也没有 Expires 首部,缓存可 以 计算出一个试探性最大使用期。可以使用任意算法,但如果得到的最大使用期大于24小时,就应该向响应首部添加一个 Heuristic Expiration Warning首部。据我们所知,很少有浏览器会为用户提供这种警告信息。
LM-Factor 算法是一种很常用的试探性过期算法,如果文档中包含了最后修改日期, 就可以使用这种算法。LM-Factor算法 将最后修改日期作为依据,来估计文档有多么易变。算法的逻辑如下所示。
• 如果已缓存文档最后一次修改发生在很久以前,它可能会是一份稳定的文档,不太会突然发生变化,因此将其继续保存在缓存中会比较安全。
• 如果已缓存文挡最近被修改过,就说明它很可能会频繁地发生变化,因此在与服 务器进行再验证之前,只应该将其缓存很短一段时间。
这段描述已经解决了个人的疑惑了,简单说算法猜你近期没有改的不会突然改了,反之认为你会突然改。在《HTTP权威指南》内有简单的公式和图例,有兴趣的可以自行前往翻阅。
个人注:《HTTP权威指南》有这样一段话:
HTTP 有一组非常复杂的新鲜度检测规则,缓存产品支持的大量配置选项,以及与 非 HTTP 新鲜度标准进行互通的需要则使问题变得更加严重了。本章其余的大部分 篇幅都用于解释新鲜度的计算问题。
所以,这个新鲜度校验是非常复杂的,远不是网上大多数文章提到的那么简略,如果以后在新鲜度校验这里出现疑问,不妨翻一翻。
结尾
问题到这里算是解决了,解决方案很简单,加上了Cache-Control和Expires。但是这次的服务器错配的导致的问题的探索,着实是很大程度上提高了个人对HTTP缓存的理解。
参考文章
《翻译:web制作、开发人员需知的Web缓存知识》
【Web缓存机制系列】2 – Web浏览器的缓存机制
《HTTP权威指南》