博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
HTTP缓存控制
阅读量:6245 次
发布时间:2019-06-22

本文共 4359 字,大约阅读时间需要 14 分钟。

hot3.png

客户端从服务器请求数据经历如下基本步骤:                                            

  1、如果请求命中本地缓存则从本地缓存中获取一个对应资源的"copy"; 

  2、检查这个"copy"是否fresh,是则直接返回,否则继续向服务器转发请求。 
  3、服务器接收到请求,然后判断资源是否变更,是则返回新内容,否则返回304,未变更。 
  4、客户端更新本地缓存。 

  no-cache:强制客户端跳过步骤2,直接向服务器发送请求。也就是说每次请求都必须向服务器发送。 
  must-revalidate:意味着某个资源在本地已缓存时长短于 max-age 指定时长时,可以直接使用,否则就要发起验证。
  no-store:缓存将不存储response,包括header和body。测试结果表明,除每次请求都必发送到服务器外,响应代码均是200,且request并没有发送"If-Modified-Since"和"If-None-Match"头,这意味着缓存的确没有存储response。 
  以上三者都是要求客户端每次请求都必须到服务器进行revalidate,此功能还可以通过max-age=0实现

 

使用缓存会带来巨大的性能提升,还能节省带宽、减少服务端开销,但很多网站对缓存一知半解,让相互依赖的资源出现竞态条件,从而无法同步更新。

使用缓存的最佳实践大体上可以归纳为这两种模式:

模式一:不变内容 + 长时间 max-age

Cache-Control: max-age=31536000

  • URL 对应的内容绝对不会改变,因此:
  • 浏览器 / CDN 直接将这个资源缓存一年也没问题;
  • 在 max-age 指定时间内,缓存副本可以直接使用,不需要与服务端协商;

页面:嘿,我需要 "/script-v1.js"、"/styles-v1.css" 和 "/cats-v1.jpg"。(10:24)

缓存:我这儿都没有。 服务端 ,你有么?(10:24)

服务端:当然,给你。对了 缓存 ,把这些资源存起来,一年之内直接用吧。(10:25)

缓存:多谢!(10:25)

页面:赞哦!(10:25)

第二天

页面:嘿,这次我要 "/script- v2 .js"、"/styles- v2 .css" 和 "/cats-v1.jpg"。(08:14)

缓存:我只有其中一个,先拿去用,剩下的我没有。 服务端 ,看看?

服务端:好,给你新的 CSS 和 JS 文件。对了 缓存 ,这些你也留着用一年吧。(08:15)

缓存:超赞!(08:15)

页面:多谢!(08:15)

后来

缓存:嗯,"/script-v1.js" 和 "/styles-v1.css" 有一阵子没被用到。我决定删掉它们。(12:32)

在这种模式中,绝对不会修改某个 URL 的内容,只会改变 URL 本身:

上面每个 URL 都包含了与文件内容同步修改的部分。这部分可以是版本号、修改时间,或者是文件内容的 MD5 —— 我的博客就是这么干的。

大部分服务端框架都有对应工具来轻松完成这项工作(我在用  ),还有一些很轻量的 Node.js 库可以实现同样功能,例如  。

但是,文章或博客详情这类 HTML 页面不适用于这种模式。这些页面 URL 不能包含版本号,页面内容还必须能修改。尤其是发现有拼写错误或语法错误后,需要快速频繁地更新。

模式二:可变内容,每次都走服务端验证

Cache-Control: no-cache

  • URL 对应的内容可能会改变,因此:
  • 没有服务端的指示,不能使用本地缓存的任何版本;

页面:嘿,我需要 "/about/" 和 "/sw.js" 两份资源。(11:32)

缓存:我搞不定。 服务端 ,看看?(11:32)

服务端:哈,我有,给你。 缓存 ,你可以自己存一份,但用之前要先问我哈。(11:33)

缓存:明白!(11:33)

页面:多谢!(11:33)

第二天

页面:我又想要 "/about/" 和 "/sw.js"。(9:46)

缓存:等一下。 服务端 ,我能直接用我本地的副本吗?在我这儿,"/about/" 最后修改于周一,"/sw.js" 最后修改于昨天。(9:46)

服务端:"/sw.js" 在那之后就没改过呢。(9:47)

缓存:赞! 页面 ,给你 "/sw.js"。(9:47)

服务端:但 "/about/" 改过,我给你最新的版本。 缓存 ,跟之前一样,你可以自己存一份,但用之前要先问我~(9:47)

缓存:了解!(9:47)

页面:赞噢!(9:47)

注: no-cache 并不是说「不缓存」,它意味着使用缓存前必须检查(或者说 验证)这个资源在服务端是否有更新。 no-store 用来告知浏览器完全不要缓存这个资源。类似的, must-revalidate 并不是说「每次都要验证」,它意味着某个资源在本地已缓存时长短于 max-age 指定时长时,可以直接使用,否则就要发起验证。好,这下明白了。

这种模式下,也可以给资源响应加上 ETag (资源的版本 ID)、 Last-Modified 时间这两个头部。下次客户端请求这些资源时,会通过 If-None-Match 或 If-Modified-Since 这两个请求头带上之前的值,这样服务端就可以返回「直接用你之前缓存的版本吧,它们是最新的」,换成行话就是「HTTP 304」。

如果服务端没办法发送 ETag / Last-Modified 头部,那每次都需要发送完整的响应内容。

这种模式下,每次都会产生网络请求,所以它没有能节省网络请求的模式一好。

模式一被基础设施影响,模式二被网络请求影响,都是常见的事儿。所以又有了中间方案:给可变内容加上短一点的 max-age 。这个折中方案太太太糟糕了。

给可变内容加上 max-age,通常是错的

不幸的是这种做法并不罕见,Github pages 当前就是这样。

假设这样三个资源:

  • /article/
  • /styles.css
  • /script.js

都有这样的响应头:

Cache-Control: must-revalidate, max-age=600

  • URL 对应的内容发生了改变;
  • 如果浏览器有一个十分钟之内的缓存副本,就不会与服务端协商,直接使用;
  • 否则发起网络请求,可能的话还会带上 If-Modified-Since 和 If-None-Match 请求头;

页面:嘿,我需要 "/article/"、"/script.js" 和 "/styles.css"。(10:21)

缓存:我这里没有, 服务端 ?(10:21)

服务端:没问题,给你。对了 缓存 ,这些资源你可以存 10 分钟。(10:22)

缓存:明白!(10:22)

页面:多谢!(10:22)

6 分钟后

页面:嘿,我又想要 "/article/"、"/script.js" 和 "/styles.css"。(10:28)

缓存:天哪,真抱歉,我把 "/styles.css" 给弄丢了,其它的都有,先给你。 服务端,你能再把 "/style.css" 发给我吗?(10:28)

服务端:当然可以,实际上它在你上次请求之后发生了改变。同样,你又可以把它缓存 10 分钟。(10:29)

缓存:没问题。(10:29)

页面:多谢!等等!彻底挂了!!发生什么啦?(10:29)

这种场景在测试环境可以构造出来,但在真实环境中难复现,也难追查。在上述例子中,实际上服务端同时更新了 HTML、CSS 和 JS,但是页面最终从缓存中拿到旧的 HTML 和 JS,并从服务端拿到最新的 CSS。版本不匹配导致功能异常。

通常,当我们对 HTML 改动很大时,很可能 CSS 需要为新结构作出调整,JS 也需要配合 CSS 和 HTML 改动而进行相应修改。这些资源相互依赖,但无法通过缓存头反映出来。最终页面可能会拿到一部分新资源,一部分旧资源。

max-age 是响应时间的相对值,某个页面上的所有资源请求,会被设置在大致相同的时间后失效,但仍有小概率出现竞争。如果你有一些不包含 JS、或包含不同 CSS 的页面,过期时间可以不同步。更为糟糕的是,浏览器一直都在淘汰缓存的资源,它不可能知道某些 HTML、CSS 和 JS 相互有依赖,所以会出现部分淘汰的情况。综上所述,最终页面拿到版本不匹配的资源并非不可能发生。

对于用来来说,这会破坏页面布局和/或功能,从小问题到大事故都有可能发生。

谢天谢地,我们有个解决方案。。。

刷新之后通常就好了

刷新页面,会让浏览器向服务端发起验证,忽略 max-age 。所以如果用户对 max-age 的这个问题很有经验的话,点击刷新按钮就能解决一切问题。当然,要求用户这样做会降低用户对你的信任,会让用户觉得你的网站很不稳定。

谨慎地给可变内容加上 max-age,能带来受益

给经常改变的内容设置 max-age 通常是错误的选择,但也不全是。现在你看到的这个页面(译者注:指原文页面)就设置了三分钟的 max-age 。在这里竞态条件不是问题,因为这个页面不依赖任何使用同样缓存模式的资源(我的 CSS、JS 及图片都属于模式一:不变内容),也不被其它使用同样缓存模式的页面所依赖。

这种模式意味着,如果我足够幸运写了一篇大受欢迎的文章,我的 CDN(Cloudflare)会帮我的服务器抗住流量,只要我能接受修改文章要等三分钟才能被用户看到,我确实可以接受。

这种模式用起来也没那么容易。如果我给一篇文章增加了一段内容,再在另外一篇文章中指向它,我就在页面之间创建了会引入竞争的依赖关系。用户可能点击链接后看到的是不包含新增内容的缓存副本。要避免这种问题发生,我必须在更新文章后,去 Cloudflare 的控制台刷新这篇文章的缓存,等三分钟再在其它文章加上指向它的链接。是的,你必须非常小心地使用这种模式。

只要用法恰当,缓存能极大的提升性能、节省带宽。让不变内容可以轻松改变 URL,让可变内容走服务端验证。如果你很勇敢,当你能确认你的内容既不依赖别人也不被别人依赖时,才针对可变内容使用 max-age ,因为它可能无法同步更新。

 

谷歌浏览器刷新模式如下:

重新载入:不额外添加cache-control

f5或刷新按钮:cache-control:max-age=0 发送确认请求  检查资源是否修改

ctrl+f5:             cache-control:no-cache        并清空缓存
                        pragma:no-cache 

转载于:https://my.oschina.net/u/2965390/blog/761513

你可能感兴趣的文章
Java设计模式--享元模式
查看>>
码栈开发手册(五)---可视化方式开发(模块详解--浏览图)
查看>>
每天一个设计模式之装饰者模式
查看>>
基于自定义日志打印的UDAF调试
查看>>
JVM源码分析之Metaspace解密
查看>>
CSS 各种定位(position)方式的区别
查看>>
每周聚划算 超值软件汇总:云市场迎新年大礼包 专场五折封顶劲省2100元
查看>>
【区块链之技术进阶】扒一扒某乎上面对于区块链的理解(二)
查看>>
如何从PostgreSQL源码分析哪些操作需要超级用户权限 - 阿里云rds superuser提供了哪些权限...
查看>>
用java进行面向对象编程,面向对象是什么意思
查看>>
博拉科技浅谈中国企业的智能制造之路
查看>>
[LeetCode]--29. Divide Two Integers
查看>>
php如何获取原生请求体
查看>>
java web开发 高并发处理
查看>>
PHP 高级编程之多线程(二)
查看>>
ART世界探险(12) - OAT文件分析(2) - ELF文件头分析(中)
查看>>
AFNetworking 和 ASIHTTPRequest
查看>>
Qt之自定义界面(实现无边框、可移动)
查看>>
MS SQL修改数据库名称
查看>>
【RMAN】使用RMAN duplicate复制同机数据库
查看>>