阅读 169

http 缓存探讨

业务场景:最近报了个问题,就是有个项目迭代更新时,有部分用户报进入后会白屏,必须ctrl+F5强制刷新才可以拉到最新的。看了后发现白屏原因是因为用户在我更新后访问的html页面还是旧的页面,所以里面的js引用的是旧的js(md5后缀),而旧的js地址在我更新后会删除,所以404,页面显示白屏...

一开始我加了以下的代码:

<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
复制代码

是的,无效,其实这段对不同的浏览器效果有差,不建议使用。


HTTP首部字段结构

只列出部分

1.通用首部字段(请求报文和响应报文都会使用的头部):

  • Cache-Control:控制缓存的行为

    Cache-Control

    Cache-Control: max-age=<seconds>
    Cache-Control: max-age=<seconds>
    Cache-Control: max-stale[=<seconds>]
    Cache-Control: min-fresh=<seconds>
    Cache-control: no-cache :强制向服务器进行验证(禁止强缓存,协商缓存可用)
    Cache-control: no-store :禁止使用缓存
    Cache-control: no-transform
    Cache-control: only-if-cached:从缓存获取资源
    复制代码
  • Date:创建报文的日期时间

2.请求首部字段

  • if-Modified-Since:比较资源的更新时间

3.响应首部字段

  • ETag:资源的匹配信息

4.实体首部字段

  • Expires:实体主体过期的日期时间
  • Last-Modified:资源的最后修改日期

浏览器缓存分类

目前主流的浏览器缓存分为两类,强缓存和协商缓存,它们的匹配流程如下:

(1)浏览器发送请求前,根据请求头的expires和cache-control判断是否命中强缓存策略,如果命中,直接从缓存获取资源,并不会发送请求。如果没有命中,则进入下一步。

(2)没有命中强缓存规则,浏览器会发送请求,根据请求头的last-modified和etag判断是否命中协商缓存,如果命中,直接从缓存获取资源。如果没有命中,则进入下一步。

(3)如果前两步都没有命中,则直接从服务端获取资源。

cache1.png

发现新打开页面时Status Code都是200 (from disk cache)。然后在此页面上刷新就是304。

强缓存

从from disk cache得知,其实浏览器一直拿的自己的缓存,所以导致根本没有拉到我最新更新的文件,这时只有强制刷新才能拿到(200)。这个状态是强缓存的结果,有expires/cache-control控制:

1.expires(http 1.0版有效)是绝对时间;是http1.0的功能。如果浏览器的时间没有超过这个expires的时间,代表缓存还有效,命中强缓存,直接从缓存读取资源。不过由于存在浏览器和服务端时间可能出现较大误差,所以在之后http1.1提出了cache-control。

2.cache-control的值是相对时间,当浏览器第一次请求资源的时候,会把response header的内容缓存下来。之后的请求会先从缓存检查该response header,通过第一次请求的date和cache-control计算出缓存有效时间。如果浏览器的时间没有超过这个缓存有效的时间,代表缓存还有效,命中强缓存,直接从缓存读取资源。

Expires用时刻来标识失效时间,不免收到时间同步的影响,而Cache-Control使用时间间隔很好的解决了这个问题。 但是 Cache-Control 是 HTTP1.1 才有的,不适用于 HTTP1.0,而 Expires 既适用于 HTTP1.0,也适用于 HTTP1.1,所以说在大多数情况下同时发送这两个头会是一个更好的选择,当客户端两种头都能解析的时候,会优先使用 Cache-Control

补充一下:Chrome在高版本更新了缓存策略,原来的from cache变成了from disk cache(磁盘缓存)from memory cache(内存缓存)

协商缓存(弱缓存)

后面在页面上刷新拿到的304,实际上就是协商缓存的结果。

配置Last-Modified/ETag的情况下,浏览器再次访问统一URI的资源,还是会发送请求到服务器询问文件是否已经修改,如果没有,服务器会只发送一个304回给浏览器,告诉浏览器直接从自己本地的缓存取数据;如果修改过那就整个数据重新发给浏览器;

你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag(实体标识)呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

  1. Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的新鲜度
  2. 如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存
  3. 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形

Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。Etag的服务器生成规则和强弱Etag的相关内容可以参考,《互动百科-Etag》和《HTTP Header definition

一般情况下,使用Cache-Control/Expires会配合Last-Modified/ETag一起使用,因为即使服务器设置缓存时间, 当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时Last-Modified/ETag将能够很好利用304,从而减少响应开销。

强缓存与协商缓存总结

  1. 强缓存阶段:先在本地查找该资源,如果发现该资源,并且其他限制也没有问题(比如:缓存有效时间),就命中强缓存,返回200,直接使用强缓存,并且不会发送请求到服务器。(不对服务器发送请求)
  2. 弱缓存阶段:在本地缓存中找到该资源,发送一个http请求到服务器,服务器判断这个资源没有被改动过,则返回304,让浏览器使用该资源。(需要发送请求,验证是否可以使用本地缓存)
  3. 缓存失败阶段(重新请求):当服务器发现该资源被修改过,或者在本地没有找到该缓存资源,服务器则返回该资源的数据。

新建页面会先验证强缓存。

F5刷新导致强缓存失效。

ctrl+F5 所有缓存都失效。

启发式缓存

那么问题肯定是缓存造成的,为什么会出现这样的情况,就可以排查了。

发现第一次访问200 (from disk cache)的时候,请求头(Request Header)显示Provisional header are shown,相关信息不能查看。

有这个提示通常可能的情况是:

  1. 跨域,请求被浏览器拦截
  2. 请求被浏览器插件拦截
  3. 服务器出错或者超时,没有真正的返回
  4. 强缓存from disk cache或者from memory cache,此时也不会显示

这里就是第4种情况。请求头不带缓存相关字段,如果本地缓存版本有效,从缓存读取,不发请求,并显示个假请求头。

找到了一篇很受启发的文章:

【腾讯Bugly干货分享】彻底弄懂 Http 缓存机制 - 基于缓存策略三要素分解法

因为这里的请求头不带缓存相关字段,所以使用了缓存过期的策略,影响要素在于:在没有任何缓存相关字段的情况下,客户端计算响应头中2个时间字段Date和Last-Modified之间的差值,取该值的10%作为缓存过期周期。这段时间内会直接使用本地缓存数据而不会再去请求服务器(强制请求除外),缓存过期后,会再次请求服务端,并携带上 Last-Modified 指定的时间去服务器对比并根据服务端的响应状态决定是否要从本地加载缓存数据。

接着对比实际的返回头发现,问题果然出现在Last-Modified。在这个问题页面里,Last-Modified的这个时间值竟然不会变,似乎一直停留在了5月份。

后来发现似乎是gitlab ci构建镜像输出的生产文件时间有误,经过修正又恢复了正常。。。

cookie和session

cookie和session都是为了弥补http协议的无状态特性,对server端来说无法知道两次http请求是否来自同一个用户,利用cookie和session就可以让用户只登录一次,server就知道某个请求是否需用重新登录。

  1. cookie:客户端第一次正常访问服务器,服务器在response headers中返回与用户信息相关的cookie,客户端收到后把cookie保存在本地,下次再发请求时会在request headers中带上这个cookie,服务器收到这个cookie就知道用户状态了。cookie可以设置过期时间,默认值是-1,表示关闭浏览器时cookie就会失效,值为0时表示立马失效,相当于删除cookie(cookie没有删除的方法),服务器和客户端都可以设置cookie,但不可以操作另一个域名下的cookie。

  2. session: 客户端第一次正常访问服务器,服务器生成一个sessionid来标识用户并保存用户信息(服务器有一个专门的地方来保存所有用户的sessionId),在response headers中作为cookie的一个值返回,客户端收到后把cookie保存在本地,下次再发请求时会在request headers中带上这个sessionId,服务器通过查找这个sessionId就知道用户状态了,并更新sessionId的最后访问时间。sessionId也会可以设置失效时间,比如如果60分钟内某个session都没有被更新,服务器就会删除这个它。

所有,cookie是保存在客户端,session是存在服务器,session依赖于cookie。


备注:docs.gitlab.com/ee/ci/yaml/… (强制gitlab ci时候每次都通过clone来获取代码,以确保拿到代码时间戳是构建开始时间):

variables:
  GIT_STRATEGY: clone
复制代码

其他参考:

《图解HTTP》

Web缓存机制系列

浅聊HTTP缓存 (HTTP Cache)

文章分类
前端
文章标签