1. 前言
之前以为只有静态资源,浏览器才会使用到缓存。但最近在做 api 流量优化时,发现 API 请求也会有缓存来减少服务器端的带宽压力的。
其中发生作用的就是 ETag,也是这里重点讲述的内容。顺便通过该知识点,把 HTTP Cache 的知识点也过了一下。
2. Browser & Server 通讯
浏览器与服务器通讯,其中 request & response 的内容分别包含了:
Browser Request
浏览器请求信息,一般分为以下部分:
- General
主要包含了 URL、HTTP Method,如:
Request URL: https://www.baidu.com/img/bd_logo1.png?where=super
Request Method: GET
- Headers
定义了信息来源、浏览器支持、缓存策略等。这里的信息一般是给 WEB 应用服务器解析的。
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7
Cache-Control: no-cache # 这是因为使用了浏览器强刷,所以 no-cache
Connection: keep-alive
Cookie: PSTM=1545632734;省略...
Host: www.baidu.com
Referer: https://www.baidu.com/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
- Parameters
Headers 更多是通讯标准定义的信息,自定义的信息内容(类似投递的信封内的信息)一般放在 Parameters。这信息更多是给程序解析的。
where=super
Server Response
服务器收到了浏览器请求,处理然后响应,一般分为以下部分:
- General
Status Code: 200 OK
- Headers
一般包含信息的类型、大小、缓存策略、WEB应用服务器等信息。
Accept-Ranges: bytes
Cache-Control: max-age=315360000
Connection: Keep-Alive
Content-Length: 7877
Content-Type: image/png
Date: Mon, 15 Jul 2019 06:50:14 GMT
Etag: "1ec5-502264e2ae4c0"
Expires: Thu, 12 Jul 2029 06:50:14 GMT
Last-Modified: Wed, 03 Sep 2014 10:00:27 GMT
Server: Apache
- Body
响应回复的信息内容。
HTTP 通讯过程中,浏览器 与 服务器主要是解读和处理 headers 部分,Request parameters & Response body 主要是服务于具体业务部分,由程序解读。
是否应用 cache,也是通过 headers 的信息解析确定。
3. Cache
Cache 台湾翻译为 快取,内地叫 缓存。
通过 cache,可以节省流量、时间等,减少资源的消耗。
举例子,一个电子商城,首页有上百张图 和 API请求,如果没有缓存,每次进来都需要重新加载所有内容,流量是相当惊人的,会加重服务器的带宽压力同时也会导致用户下载等待过久影响体验。
3.1 Cache 如何发生作用?
浏览器通过检测请求的资源没有==过时== 或者 ==内容变化==,则从 cache 中取 response body。
这里可能有些人会有误区,缓存策略是不是意味着不请求服务器?其实并不是,是会根据上面写的触发 cache 的原因而有不同。
a) 按资源的有效期判断
如图所示,有部分资源服务器第一次响应时会返回 max-age 或者 expires 告知浏览器,该资源在该时间范围内不产生变化。浏览器收到该信息后,下次请求时,会先检查 max-age 和 expires,如果在有效时间内,则直接从 cache 中读取。
- 应用场景:静态资源,如图片、js、css、html
- Status Code:200 OK (from memory cache)
- 缓存读取方式:memory cache (有生命周期的)
b) 按资源的内容判断
当资源不满足 (a) 的缓存策略,无法从 memory cache 中获取数据时,并不意味着缓存策略失效,还可以通过与服务器沟通来进一步确定。(a) 中的活动图按(b)进一步拓展为 (注意活动图的 Before max-age 与 Before expired date 不一定存在,response 存在时才需要判断):
- 应用场景:动态API、动态页面、过期的静态资源
- Status Code: 304 Not modified (from disk cache)
- 缓存读取方式:disk cache
3.2 影响 cache 的 headers 因素
上一章节已经讲述了 cache 生效的机制,这里进一步对生效中判断的因素展开讲解。
为了进行学习,特意找了一个 API 进行个例分析(去掉了一些非相关的项):
第一次请求:
General:
Request URL: https://cd3.lcola.cn/backend/charge_stations
Request Method: GET
Status Code: 200 OK
Referrer Policy: no-referrer-when-downgrade
Response Headers:
Cache-Control: max-age=0, private, must-revalidate
Date: Mon, 08 Jul 2019 09:05:48 GMT
# 请求有返回 Entity Tag
ETag: W/"25d6a32c560a93be6f4f3ba69f6353a6"
Request Headers:
Cache-Control: no-cache
Connection: keep-alive
Pragma: no-cache
第二次请求:
General:
Request URL: https://cd3.lcola.cn/backend/charge_stations
Request Method: GET
# 第二次状态码不同
Status Code: 304 Not Modified
Referrer Policy: no-referrer-when-downgrade
Response Headers:
Cache-Control: max-age=0, private, must-revalidate
Date: Mon, 08 Jul 2019 09:06:06 GMT
# Entity Tag 不变
ETag: W/"25d6a32c560a93be6f4f3ba69f6353a6"
Request Headers:
# 请求的头部信息,有对于 ETag 的判断
If-None-Match: W/"25d6a32c560a93be6f4f3ba69f6353a6"
通过上面的可以看到核心关键的 Response ETag 与 Request If-None-Match。
开始来看看 浏览器 与 服务器如何通过 request 与 response 的 Pragma, Cache-Control, Etag, Last-Modified, Expires 进行缓存机制。
a) Expires
Response Headers:
Expires: Wed, 21 Oct 2017 07:28:00 GMT
Cache 的到期时间(具体的日期与时间),由服务器端定义。
浏览器收到 response header 后,会将资源存储起来,然后等到下一次访问同一请求时,会检查【当前时间】是否超过这个【Expires】。如果没有超过,则浏览器不发请求,直接从 disk cache 直接取,Status code 会标记为:Status code 200 (from disk cache)。
⚠️ 浏览器对该时间的判断会依赖于客户端,如果把时间改为 2999年,那么这个请求的缓存机制就失效了。
b) Cache-Control max-age
Response Headers:
Cache-Control: max-age=30
Cache 失效的时间(秒),通过计算下一次请求与第一次请求的的相隔时间,确定是否使用 cache。
可以有效解决 Expires 的客户端时间依赖问题。
当前 google 的首页图片就是通过 expires 与 max-age 进行设置 cache 策略:
General:
Request URL: https://www.google.com.hk/images/nav_logo299.webp
Request Method: GET
Status Code: 200 (from disk cache)
Response Headers:
cache-control: private, max-age=31536000
date: Thu, 30 May 2019 17:11:47 GMT
expires: Thu, 30 May 2019 17:11:47 GMT
last-modified: Tue, 23 Apr 2019 01:00:00 GMT
那么相同同时设置,并且假设 max-age 与 expires 的失效时间不统一,那么以哪个为准?
根据RFC2616的定义:
If a response includes both an Expires header and a max-age directive, the max-age directive overrides the Expires header, even if the Expires header is more restrictive
max-age 的设置会覆盖 Expires,同时设置时,生效的只有 max-age。
c) Last-Modified 与 If-Modified-Since
a & b 在“有效时间的缓存策略”判断中发挥作用,c & d 讲到的会在“内容变化的缓存策略”判断中发挥作用。
Response Headers:
Last-Modified: 2017-01-01 13:00:00
Cache-Control: max-age=31536000
Server 端通过 response 返回该资源的修改时间。
那么下次访问该请求时,浏览器会通过 request If-Modified-Since,将第一次访问的时间 与 Last-Modified 对比,如果没有更新,服务器会回复 ==Status code: 304 (Not Modified)==,客户端会继续使用该资源。
Request Headers:
If-Modified-Since: Fri, 05 Jul 2019 02:14:23 GMT
Last-Modified 一般会结合 ETag 一起使用,确保是同一资源没有被修改。
ETag 與 If-None-Match
对于静态资源,可以通过 Last-Modified 来识别图片是否被修改,但是对于API请求,内容基本都是根据请求参数动态生成,那么 Last-Modified 肯定是不适用。
动态API请求,假设内容相同,是否可以应用 cache 机制?
答案是:Yes。主要的实现是依赖于 reponse 的 ETag。
Response Headers:
ETag: W/"1d6f97d3adad0e99400af20a1421a974"
ETag 是 Entity Tag 的简写,是一个资源版本的唯一标识,类似于一个资源的对应的 hash code。
如果一个 response 有 ETag,那么浏览器下一次访问该资源时,会在请求的头部加上 If-None-Match,如果 Server Response 返回的 ETag 一样,那么表示内容没有变化,返回 ==Status code: 304 (Not Modified)==。浏览器就通过从 cache 中重新获取数据。
Request Headers:
If-None-Match: W/"a5759fbb890a0d32c4de53eea2264c03"
Strong v/s Weak ETags
"543b39c23d8d34c232b457297d38ad99" – Strong ETag
W/"543b39c23d8d34c232b457297d38ad99" – Weak ETag
ETag 分为 Strong 与 Weak
Strong ETag indicates that resource content is same for response body and the response headers.
Weak ETag indicates that the two representations are semantically equivalent. It compares only the response body.
Rails 4 默认使用的是 Strong ETag, Rails5 默认使用的是 Weak ETag。
4. 案例讲解
如何不使用 cache?
如果某些机密的信息,不希望留在客户端,可以 response 设置:
Response Headers:
Cache-Control: no-store
上面是指完全不适用快照,每次都需要重新像服务器请求。
而 ==Cache-Control: no-cache== 还是会请求服务器,只有当检查到页面信息变化了,才不获取缓存。等价于设置 ==max-age=0==
可以看到文章头部的例子中,强刷时浏览器会给请求加上 ==Cache-Control: no-cache==。
如何解决页面缓存导致的外部资源引用不更新?
页面的基本 html 内容不变,但是里面的 css/js/image 的信息变化了(名字没有改变),这时候很容易因为页面缓存而导致了页面上没有应用到最新的 css/js/image。
为了解决这种情况,css/js/image url 加上更新的时间戳作为参数,这样当他们变化时页面 response 也会重新生成 ETag,从而重新加载所有信息。
或者像 webpack 打包一样,资源重新生成一个带 hash 的名字。
获取资源时,Http method 该用 get 还是 post?
如果参照 restful 标准,获取资源时 http method 应使用 get,但是有时候考虑到参数过长问题,部分时候会使用 post。正常情况,只是 定义不同,获取方式不同,实际对缓存策略也是有影响的。
==获取资源能用 GET 别用 POST==
因为 GET API 请求有 cache 机制支持,所以这是为什么请求不建议使用 POST。
POST 请求不提交 If-None-Match,所以 cache 不生效。另外参数不在URL,每次请求因为参数变化导致的 response 内容变化,ETag 也变化,cache 基本作废。
5. 参考资料
《循序漸進理解 HTTP Cache 機制》
《MDN web docs - HTTP》
《W3C》
《Developers google - HTTP caching》
《plantuml》
附上面用到的 plantuml 源码:
@startuml
title HTTP Communication \n planttext.com
start
:Browser request;
note left
HTTP url & method
end note
:Check last response;
if (a. Before max-age?) then (yes)
:Get response \n__from memory cache__;
elseif (b. Before expired date?) then (yes)
:Get response \n__from memory cache__;
else (no)
:Communicate with server;
if (c. If-Modified-Since?) then (no)
:Response with\n status code 304 & no data;
:Get response\n__from disk cache__;
elseif (d. If-None-Match?) then (no)
:Response with\n status code 304 & no data;
:Get response\n__from disk cache__;
else (yes)
:Response with\n status code 200 & data;
endif
endif
:Render;
stop
@enduml