关于 HttpStatus 200(From Cache)和 304(Not Modified)的探索

作者 Gavin 日期 2015-09-01
关于 HttpStatus 200(From Cache)和 304(Not Modified)的探索

今晚走了很长时间的弯路,最后发现问题在于 url load 访问Command+R 刷新上,道路曲折但是很有意思。

问题的起源在于晚上把一个demo部署到阿里云上,在配置nginx的时候,设置了expire。但是当用Chrome检查Network的时候,发现已经设置了expire的css,js,image 总是返回304(Not Modified)的状态。因为之前服务器缓存资源出现返回304的问题,所以就打算看下到底是什么问题

首先,我断定自己的expire是没有设置错的:

1
2
3
4
location ~ .*.(css|js)$ {
expires 30d;
break;
}

于是开始查看是否是ETag的配置问题?

  • 弯路1:nginx自从1.3版本后默认带有etag,木有发现,然后去github下载第三方ETag插件编译。。。 1.3版本后可以通过设置 etag on|off实现 etag 的配置,并且默认开启etag.

Web服务器我们以nginx为例,以css文件为例,ETag可以理解为一个文件的标记,是唯一的.

ETag的工作原理是:

  • 第一次访问, nginx会返回ETag值,浏览器会记录下来

Request

1
2
3
4
5
6
7
8
9
10
Accept:text/css,*/*;q=0.1
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6
Cache-Control:no-cache
Connection:keep-alive
Host:123.57.161.203:81
Pragma:no-cache
Referer:http://123.57.161.203:81/
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.6 Safari/537.36
X-FirePHP-Version:0.0.6

Response

1
2
3
4
5
6
7
8
9
10
11
12
Status Code:200 OK
Accept-Ranges:bytes
Cache-Control:max-age=2592000
Connection:keep-alive
Content-Length:32628
Content-Type:text/css
Date:Tue, 01 Sep 2015 14:06:27 GMT
ETag:"55e57588-7f74"
Expires:Thu, 01 Oct 2015 14:06:27 GMT
Last-Modified:Tue, 01 Sep 2015 09:53:12 GMT
Server:nginx/1.8.0
  • 第二次访问时,浏览器会带着上次nginx返回的ETag值作为If-None-Match的值,发起请求,这时候nginx会把If-None-Match值与该页面对应的ETag值进行比较,如果不相等则返回200,重新下载资源;如果二者相等,则返回304,浏览器调用本地缓存。

Request

1
2
3
4
5
6
7
8
9
10
11
Accept:text/css,*/*;q=0.1
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6
Cache-Control:max-age=0
Connection:keep-alive
Host:123.57.161.203:81
If-Modified-Since:Tue, 01 Sep 2015 09:53:12 GMT
If-None-Match:"55e57588-7f74"
Referer:http://123.57.161.203:81/
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.6 Safari/537.36
X-FirePHP-Version:0.0.6

Response

1
2
3
4
5
6
7
8
9
Status Code:304 Not Modified
Cache-Control:max-age=2592000
Connection:keep-alive
Date:Tue, 01 Sep 2015 14:10:08 GMT
ETag:"55e57588-7f74"
Expires:Thu, 01 Oct 2015 14:10:08 GMT
Last-Modified:Tue, 01 Sep 2015 09:53:12 GMT
Server:nginx/1.8.0

但是比较纳闷的是为什么本地有缓存了,设置expire也成功了,浏览器不直接读取cache内容而是先去web服务器请求了一次?

正常的 from cache 的 Request(应该说没有发起请求) 和 Response 应该是这样的:

Request

1
-

Response

1
2
3
4
5
6
7
8
9
10
11
Status Code:200 OK (from cache)
Accept-Ranges:bytes
Cache-Control:max-age=2592000
Content-Length:32628
Content-Type:text/css
Date:Tue, 01 Sep 2015 14:12:56 GMT
ETag:"55e57588-7f74"
Expires:Thu, 01 Oct 2015 14:12:56 GMT
Last-Modified:Tue, 01 Sep 2015 09:53:12 GMT
Server:nginx/1.8.0

后来看到知乎的一个问题 阿里云存储如何让浏览器始终以200 (from cache)缓存图片,里面有一句话:

通过大家的回答和我自己的实验发现(Chrome上),对于阿里云的云存储,加大Cache-Control的max-age是有效的,这点我之前也试过,但是像 @yuanyuanVivian说的,是在输入URL按下回车时有效,直接刷新时图片还是无法直接加载缓存,而且无法禁用阿里云存储Etag.

受到启发,终于找到问题所在了:

  • 弯路2:URL回车或者链接访问URL 与 刷新或者强制刷新(mac下的Command+R,win下的F5,Ctrl+F5等)这两种方式浏览器的处理方式是不一样的:前者操作方式,浏览器获取资源的时候不会设置 Cache-Control:max-age=0,所以如果expire设置的max-age如果仍有效的话会优先从本地cache中获取;但是后者发起Request的时候浏览器给 header 里设置的 Cache-Control:max-age=0,可以参照上文第2次访问请求。我们都知道一旦max-age为0,则不会从本地cache获取数据了,所以会发起一次http请求,nginx根据header里传来的If-Modified-Since或者If-None-Match分别与Last-ModifiedEtag做对比,从而做出返回304还是200的选择,而强制刷新是将 hreader 设置为 Cache-Control:no-cache,直接返回200,下载资源.

所以说 设置的 expire 是生效的。正常情况用户点链接的话,属于加载,不属于刷新,缓存的静态资源肯定会优先从本地cache获取,但是刷新的时候就避免不了一次http请求获取304了。


Expire 和 ETag 都有缓存的作用,但是区别在于:

  1. Expire 第二次访问的时候会直接从 本地cache获取,即 200(From Cache),ETag会发起一次http请求,最后返回304(Not Modified)

  2. 分布的问题:因为Expire第二次访问不会发起http请求,所以不存在前后资源访问不在同一台机器上的问题;但是由于每台机器在某一个时刻针对某一个资源生成ETag的值不一样,将会导致如果第二次请求分配到了另外的机器,If-None-Match与另外的服务器ETag值不对应,则重新下载资源,导致本地缓存失效!

所以在有多台web服务器的情况下要优先使用Expire.

具体nginx对Expire和Etag使用优先级是什么?这个只能阅读源码,以后探索了

参考资料:

  1. Module ngx_http_core_module
  2. HTTP头信息中的参数Etag
  3. 200 OK (FROM CACHE) 与 304 NOT MODIFIED
  4. 阿里云存储如何让浏览器始终以200 (from cache)缓存图片?
  5. Best Practices for Speeding Up Your Web Site
  6. HTTP status code 200 (cache) vs status code 304?
  7. Reload vs. Refresh in Firefox (Cache-Control)