HTTP缓存及其使用
(以前以为HTTP缓存是个简单的事,项目中遇到后才发觉关于缓存实践有挺深的学问)
0. What
这里的缓存指的是http标准中定义的缓存技术(如Cache-Control),主要由服务端设置和处理(当然还要由客户端旳浏览器配合)而不需要前端开发者参与。当做了恰当的参数设置后客户端发起请求过程中缓存会由浏览器和服务端间自动进行处理完成,而不用用户参与。
当前HTML5 API中有LocalStorage、SessionStorage技术也被用于“缓存”,然而这类本质上是种类似于DB的本地存储,由开发者进行控制。这种不是我们这里所要讨论的缓存。
整个 Web 系统架构在 HTTP 协议 之上, 利用 HTTP 的缓存机制不仅可以极大地减少服务器负载, 更重要的是加速页面的载入以及减少用户的流量消耗。 HTTP缓存机制也早已广泛地被服务器厂商(如 Tomcat、Apache、Virgo)和浏览器厂商(如Chrome、Firefox、Safari)实现。此外,一些服务器端框架(如 Django、Express.js)也实现了 HTTP 缓存机制。
1. 两类缓存
浏览器和服务器之间使用的缓存策略可以分为强缓存、协商缓存两种,通常一起搭配使用。
强缓存:用户发送的请求,直接从用户客户端缓存读取,不发送到服务端,无与服务端交互
协商缓存:用户发送的请求,发送到服务端,由服务端根据参数判断是否让客户端从客户端缓存读取。协商缓存无法减少请求开销,但可减少返回的正文大小
相同点:最终都是从客户端缓存读取而不用服务端发送请求的数据回来;
不同点:客户端的请求是否发送到服务端,即是否与服务端交互
适用性:强缓存适用于很少变化的资源(如封面图、js文件等这些静态文件),可以把过期时间设长;协商缓存适用于经常变化的资源(如网页页面内容)。
2. 相关参数
与强缓存和协商缓存相关的HTTP Header参数如下:(注:HTTP标准中Header名首字母大写)
Date:用于记录此次缓存时服务器的时间
Expires、Cache-Control、Pragma:用于强缓存的判断
Last-Modified、 If-Modified-Since、Etag、 If-None-Match:用于协商缓存判断,以实现有条件的HTTP请求
后面用到时详细介绍。
注:
Pragma no-cache优先级高于Cache-Control的no-cache、Cache-Control的max-age优先级高于Expires
Etag优先级高于Last-Modified
3. 整体过程
整体过程如下,若(b)是N则是使用强缓存、若(c)为Y则是使用协商缓存。
整体过程可理解为:
- 发送请求前浏览器先检查本地是否有缓存,没有则直接向服务器请求资源;
- 若本地有缓存且尚未过期(根据请求头的Expires和Cache-Control)则直接取缓存,若过期则向服务端发送请求;
- 服务端收到请求后判断是否仍让客户端使用缓存(根据请求头的Last-Modified和Etag,例如文件没变化,服务端可让浏览器直接用缓存而不用重新发文件给用户)、若是则返回304告诉客户端直接取缓存
- 若以上两个缓存都没命中,则浏览器再请求服务器获取最新资源,服务器返回资源的同时设置一些上述缓存参数
(a)浏览器判断是否有缓存
浏览器会在系统某个位置专门存放缓存信息。通过检查该位置是否有对应请求的缓存信息来判断是否有缓存(对于Chrome 66之前的版本也可在chrome://cache 查看请求及对应的缓存信息)。
缓存信息包括请求的响应头及对应的缓存内容。一个缓存示例如下:
(b)判断缓存是否过期
客户端检查到本地有缓存的话会判断缓存是否过期。
缓存信息中包含被缓存的请求的响应头。里面包含Date、Cache-Control、Expires、Pragma字段(可能不是每个都有)用于判断缓存是否过期。下面介绍各字段的作用再说明判断方法。
各字段旳作用:
Date:指明此次缓存的时间(服务器时间)
Expires:指明缓存过期的绝对时间(服务器时间),如 Thu, 28 Sep 2017 06:38:37GMT 。http 1.0的标准。存在的问题:客户端服务端时间不一致可能导致缓存效果不符合期望。
Cache-Control:指明缓存过期策略,可看成是Expires的补充,使用相对时间。http 1.1的标准。属性设置:
- max-age: 设置普通缓存的最大有效时间(单位为s)。max-age会覆盖掉Expires
- s-maxage: 只用于共享缓存如CDN缓存(单位为s)。与max-age 的区别:max-age用于普通缓存而s-maxage用于代理缓存。s-maxage会覆盖max-age 和 Expires设置
- public:响应会被缓存,且在多用户(如多个浏览器)间共享。未指定public或private则默认是public
- private: 响应只作为私有缓存,不能在用户间共享。如果要求HTTP认证,响应会自动设置为private
- no-cache: 指定不缓存响应,表明资源不进行缓存。设置了no-cache之后并不代表浏览器不缓存,而是在缓存前要向服务器确认资源是否被更改。故有时只设置no-cache防止缓存不够保险,还可加上private指令,将过期时间设为过去的时间
- no-store: 绝对禁止缓存,每次请求资源都会从服务端重新获取。如SpringMVC的默认异常处理中(AbstractHandlerExceptionResolver)会对response设此属性值。
- must-revalidate: 如果页面过期,则去服务器进行获取。不常用
Cache-Control的设置规则:
Pragma:只有 Pragma: no-cach 一种用法,与Cache-Control:no-cache的作用一样。出现原因:http 1.0没有实现no cache的功能,因此用Pagama使no cache功能应用到1.0。Pragma会的优先级高于Cache-Control的no-cache
判断缓存是否过期的规则:
若Cache-Control中有max-age或s-maxage则用它们加上date作为过期绝对时间,否则直接用expires指定的时间作为过期绝对时间。将绝对时间与当前时间比较是否过期。若未过期则直接使用缓存(此时就是前面说的强缓存)。
(c)向服务器询问是否使用缓存
若客户端判断缓存已过期,则向服务端发送请求。服务端根据Last-Modified/If-Modified-Since、Etag/If-None-Match字段(也可能不是每个都有)判断是否让客户端用缓存。
下面同样先介绍各字段作用再说明判断方法。
各字段作用:(这几个字段通常用于有条件的HTTP请求)
Last-Modified:表明请求的服务端资源上次的修改时间(单位为s,且为服务器时间)
If-Modified-Since:客户端保留的资源上次的修改时间
Etag:服务端根据资源内容生成的一段标识(不唯一,通常为文件的md5或者hash值或版本号等,只要保证写入和验证时的方法一致即可)
If-None-Match:客户端保留的上次获取到的的Etag
判断规则(这部分通常服务器实现或服务端代码自己实现):
浏览器向服务端发送请求时,若上一次的缓存中有Last-Modified或Etag字段则在request header中加入If-Modified-Since(对应Last-Modified)或If-None-Match字段(对应If-None-Match),以询问服务端资源是否被修改过。服务端判断资源是否修改(对于If-Modified-Since服务端看自该时间后资源是否修改、对于If-None-Match服务端比较资源目前的Etag是否与所收到的一样):若未修改则服务端返回304,浏览器使用缓存;否则浏览器再次请求资源,状态码为200、资源为服务器最新资源。Etag处理流程示意图:
通常情况下,若同时发送If-None-Match、If-Modified-Since字段,服务器只要比较Etag的内容即可(即Etag的优先级高于另者),当然具体处理方式,看服务器的约定规则。
注:
使用ETag可以解决Last-modified存在的一些问题:
- 某些服务器不能精确得到资源的最后修改时间,这样就无法通过最后修改时间判断资源是否更新
- 如果资源修改非常频繁,在秒以下的时间内进行修改,而Last-modified只能精确到秒
- 一些资源的最后修改时间改变了,但是内容没改变,使用ETag就认为资源还是没有修改的
分布式系统中尽量不用Etag,因为每台机器生成的Etag都一样。(啥意思???)
分布式系统里多台机器间资源的Last-Modified必须一致,以免负载均衡不同导致对比失败
至此,结合上述参数的整个请求处理过程如下:
Chrome浏览器从本地缓存中取资源时,发起的请求中会有"from memory cache" 或 "from disk cache" 标识(最早只有from cache,从某个版本起改为此两者),示例:
Chrome from memory cache与from disk cache的区别:
Chrome employs two caches — an on-disk cache and a very fast in-memory cache. The lifetime of an in-memory cache is attached to the lifetime of a render process, which roughly corresponds to a tab. Requests that are answered from the in-memory cache are invisible to the web request API. If a request handler changes its behavior (for example, the behavior according to which requests are blocked), a simple page refresh might not respect this changed behavior. To make sure the behavior change goes through, call handlerBehaviorChanged() to flush the in-memory cache. But don't do it often; flushing the cache is a very expensive operation. You don't need to call handlerBehaviorChanged() after registering or unregistering an event listener.
4. 缓存参数设置总结
- 谨慎地使用过期时间,最好配合 MD5 一起使用。参数设置过长会导致客户端看到的一直是强缓存的旧数据,得不到及时更新。CDN资源、图片等常过期时间通常较长。
- 总是启用条件请求,比如 Etag 或 Last-Modified。
- 文件服务采用 Last-Modified,动态内容采用 Etag。
- 分离经常变化的部分,也会提高缓存的命中率。
5. 用户行为对浏览器缓存的影响
用户操作 | Cache-Control/Expires | Last-Modified/Etag |
地址栏回车 | 有效 | 有效 |
页面链接跳转 | 有效 | 有效 |
新开窗口 | 有效 | 有效 |
前进、后退 | 有效 | 有效 |
F5刷新 | 无效 | 有效 |
Ctrl + F5刷新 | 无效 | 无效 |
可见,绝大多数情况下强缓存和协商缓存都是起作用的,普通刷新时强缓存无效、强制刷新时强缓存和协商缓存都无效。
6. 实践
Java Web中设置缓存
// 设置缓存。强缓存适合不频繁变更的文件,协商缓存用于变化频繁的文件。我们的业务场景中资源很少变化,故以强缓存为主、协商缓存几乎触发不到。 { // 以下设置强缓存 int cacheTimeSecond = 5 * 3600;// 强缓存时间 Date curDate = new Date(); Date newLastModifyDate = PersistentStorageUtilFactory.getPersistStorageUtil() .getObjectMetaData(bucketName, objKey).getLastModified(); response.setHeader("Date", curDate.toGMTString()); response.setHeader("Cache-Control", "private, max-age=" + cacheTimeSecond);// second。时间太长的话可能导致前端缓存的资源与服务端资源不一致。max-age会覆盖expires response.setDateHeader("expries", curDate.getTime() + cacheTimeSecond * 1000); // 以下设置协商缓存 String newEtagStr = String.valueOf(newLastModifyDate.hashCode()); SimpleDateFormat sdf = new SimpleDateFormat(); String oldModifiedTimeStr = request.getHeader("If-Modified-Since"); String oldEtagStr = request.getHeader("If-None-Match"); boolean isModifiedTimeNotChanged = null != oldModifiedTimeStr && sdf.parse(oldModifiedTimeStr).equals(newLastModifyDate); boolean isContentNotChanged = null != oldEtagStr && oldEtagStr.equals(newEtagStr); if (isModifiedTimeNotChanged || isContentNotChanged) {// 服务器资源没有更新。返回304 response.setStatus(HttpStatus.SC_NOT_MODIFIED); return; } else { response.setHeader("Last-Modified", newLastModifyDate.toGMTString()); response.setHeader("Etag", newEtagStr); } }
如果服务端代码的接口实现中默认对所有返回的资源都设置了强缓存,但确实又不想让资源缓存了,该怎么做?让前端在链接里加上各任意参数(如时间戳)即可,这样浏览器发现参数不一样就不会用强缓存了。
7. 参考资料
https://blog.csdn.net/u014590757/article/details/80140654
http://www.alloyteam.com/2016/03/discussion-on-web-caching/
https://excaliburhan.com/post/things-you-should-know-about-browser-cache.html