起源:通过网络获取内容既缓慢,成本又高。大的响应需要在客户端和服务器之间进行多次往返通信,这样拖延了浏览器可以使用和处理内容的时间,同时也增加了访问者的数据成本。所以缓存和重用已获取的资源能够有效地提升网站与应用的性能。web缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间。借助HTTP缓存,web站点变得更具有响应性。
缓存:缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。对于网站来说,缓存是达到高性能的重要组成部分。
HTTP报文中与缓存相关的首部字段(RFC2616):
1.通用首部字段(即请求报文和响应报文都能用的字段)
字段名称 | 说明 |
---|---|
Cache-Control | 控制缓存的行为 |
Pragma | http1.0,值为“no-cache”时禁用缓存 |
2.请求首部字段
字段名称 | 说明 |
---|---|
If-Match | 比较ETag是否一致 |
If-None-Match | 比较ETag是否不一致 |
If-Modified-Since | 比较资源最后更新的时间是否一致 |
If-Unmodified-Since | 比较资源最后更新的时间是否不一致 |
3.响应头部字段
字段名称 | 说明 |
---|---|
ETag | 资源的匹配信息 |
4.实体首部字段
字段名称 | 说明 |
---|---|
Expires | http1.0,实体主体过期的时间 |
Last-Modified | 资源的最后一次修改的时间 |
当下大多数浏览器在点击刷新按钮或者按F5时会自行加上“Cache-Control:max-age=0”请求字段,后文提及的“刷新”多指的是选中URL地址栏并按回车键(这样不会被强行加上Cache-Control)。
石器时代的缓存方式(HTTP1.0)
在http1.0时代,给客户端设定缓存方式可通过两个字段——“Pragma”和“Expires”来规范。虽然这两个字段早可抛弃,但为了做http协议的向下兼容,很多网站依旧会带上这两个字段。
1.Pragma
当该字段值为“no-cache”的时候,会告诉客户端不要对该资源读缓存,即每次都得向服务器发一次请求。Pragma属于通用首部字段,在客户端上使用时,常规要求我们往html上加上这段meta元标签:
它告诉浏览器每次请求页面时不要读取缓存。但是这种禁用缓存的形式有以下缺陷:
(1)仅IE能识别这段meta标签含义,其他主流浏览器仅能识别“Cache-Control:no-store”的meta标签
(2)在IE中识别到该meta标签含义,并不一定会在请求字段加上Pragma,但的确会让当前页面每次都发送新请求。
因此,这种客户端定义Pragma的形式作用不大。但是在响应报文上加上该字段有效。
2.Expires
有了Pragma来禁用缓存,就需要有个东西来启用缓存和定义缓存时间,对于http1.0来说,Expires就是做这件事的首部字段。
Expires的值对应一个GMT(格林尼治时间),比如“Mon,22 Jul 2002 11:22:01 GMT”告诉浏览器资源缓存过期时间,如果没过该时间,则不发请求。
在客户端我们同样可以使用meta标签来告诉IE(只有IE识别)页面缓存时间:
如果希望在IE页面不走缓存,每次刷新页面都能发送新请求,那么可以把“content”的值写成“-1”或“0”。同样的,你并不能在请求或响应报文中找到Expires字段。但是如果在服务端设置Expires字段,则在任何浏览器中都能正确设置资源缓存的时间。
同时在服务端定义Pragma字段和Expires字段时,Pragma的优先级更高。但响应报文中Expires所定义的缓存时间是相对服务器上的时间而言的,如果客户端上的时间跟服务器上的时间不一致,那缓存时间可能就没有意义了。
HTTP1.1
1.Cache-Control
针对http1.0中“Expires时间是相对服务器而言,无法保证和客户端时间统一”的问题,http1.1新增了Cache-Control来定义缓存过期时间,若报文中同时出现了Pragma、Expires、Cache-Control,会以Pragma为准。也就是说优先级从高到低:Pragma<-Cache-Control<-Expires
Cache-Control也是一个通用首部字段,这意味着它能分别在请求报文和响应报文中使用。RFC规范Cache-Control的格式为:
"Cache-Control"":"cache-directive
作为请求首部,cache-directive的可选值有:
字段名称 | 说明 |
---|---|
no-cache | 告知(代理)服务器不直接使用缓存,要求向原服务器发起请求 |
no-store | 所有内容都不会被保存到缓存或internet临时文件中 |
max-age=delta-seconds | 告知服务器客户端希望接收一个存在时间(Age)不大于delta-seconds秒的资源 |
max-stale=[=delta-seconds] | 告知(代理)服务器客户端愿意接收一个超过缓存时间的资源,若有定义delta-seconds则为delta-seconds秒,若没有则为任意超出的时间 |
min-fresh=delta-seconds | 告知(代理)服务器客户端希望接收一个在小于delta-seconds秒内被更新过的资源 |
no-transform | 告知(代理)服务器客户端希望获取实体数据没有被转换(比如被压缩)过的资源 |
only-if-cache | 告知(代理)服务器客户端希望获取缓存的内容(若有),而不用向原服务器发去请求 |
cache-extension | 自定义扩展值,若服务器不识别改值将被忽略掉 |
作为响应首部时,cache-directive的可选值为:
字段名称 | 说明 |
---|---|
public | 表明任何情况下都得缓存该资源(即使是需要HTTP认证的资源) |
Private[="field-name"] | 表明返回报文中全部或部分仅开放给某些用户做缓存使用,其他用户不能缓存这些数据 |
no-cache | 不直接使用缓存,要求向服务器发起请求 |
no-store | 所有内容都不会被保存到缓存或Internet临时文件中 |
no-transform | 告知客户端缓存文件时不得对实体数据做任何改变 |
only-if-cached | 告知(代理)服务器客户端希望获取缓存的内容,而不用向原服务器发起请求 |
must-revalidate | 当前资源一定是向原服务器发去验证请求的,若请求失败会返回504(Gateway Time-out) |
proxy-revalidate | 与must-revalidate类似,但仅能应用于共享缓存 |
max-age=delta-seconds | 告知客户端该资源在delta-seconds秒内是新鲜的,无需向服务器发请求 |
s-maxage=delta-seconds | 同max-age,但仅应用于共享缓存 |
cache-extension | 自定义扩展值,若服务器不识别该值就会被忽略 |
我们依旧可以在HTML的meta标签来给请求报头加上Cache-Control字段,另外,Cache-Control允许自由组合可选值:
Cache-Control:max-age:3600,must-revalidate
它表示该资源是从原服务器上获取的,且其缓存(新鲜度)的有效时间为1小时,在后续1小时内,用户重新访问该资源无需发送请求。
2.缓存校验字段
上述的首部字段均能让客户端决定是否向服务器发送请求。如果设置的缓存时间未过期,则直接从本地缓存读取数据即可,若缓存时间过期了或资源不直接走缓存,则会发送请求道服务器。
现在的问题是,如果客户端向服务器发送了请求,是否就一定要读取该资源的整个实体内容?针对案例——客户端上某个资源保存的缓存时间过期了,但这时候其实服务器并没有更新过这个资源,如果这个资源数据量很大,客户端要求服务器再把这个东西重新发送一遍,就非常浪费时间和带宽。但是,如果想办法让服务器知道客户端存有的缓存文件其实是跟自己所有的文件是一致的,然后高速客户端“这东西你直接用缓存里就可以了,我这边还没更新,就不再传一次过去了”。
为了让客户端和服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,Http1.1新增了几个首部字段来做这件事。
2.1Last-Modified
服务器将资源传递给客户端时,会将字原最后更改的时间以“Last-Modified GMT”的形式加在实体首部上一起返回给客户端。
客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并发给服务器做检查,若传递的时间值与服务器上该资源最终修改时间一致,这说明该资源没有被修改过,直接返回304状态码(Not-Modified)
(1)If-Modified-Since:Last-Modified-value
该请求首部告诉服务器如果客户端传来的最后修改时间与服务器上一直,则直接返回304和响应包头即可
(2)If-Unmodified-Since:Last-Modified-value
高速服务器若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应返回412(Precondition Failed).
2.2ETag
Last-Modified并不是没有缺点,如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回整个实体给客户端。
为了解决上述Last-Modified可能存在的不准确的问题,Http1.1推出了ETag实体首部字段。
服务器会通过某种算法给资源计算出一个唯一的标志符,再把资源响应给客户端的时候,在实体首部加上“ETag:唯一标识符”一起返回给客户端。
客户端会保留该ETag字段,并在下一次请求时将其一并发给服务器。服务器只需比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能判断出该资源相对客户端是否被修改过。若服务器匹配ETag不一致,则直接以常规GET 200将新的资源发给客户端;若匹配一致,则直接返回304告知客户端直接使用本地缓存即可。
(1)If-None-Match:ETag-value
客户端请求首部字段,告诉服务器如果ETag没有匹配上需要重新发送资源数据,否则直接返回304和响应报头
(2)If-Match:ETag-value
客户端请求首部字段,告诉服务器如果没有匹配到ETag,或收到了“*”而当前没有该资源实体,则返回412(Preconditioned Failed)给你客户端,否则服务器直接忽略该字段。
缓存实践
在实际做http缓存的应用时,我们还是会把上述的大多数首部字段均用上,使用Expires来兼容旧的浏览器,使用Cache-Control来更精确的利用缓存,然后开启ETag和Last-Modified功能进一步复用缓存减少流量。
缓存头部对比
头部 | 优势和特点 | 劣势和问题 |
---|---|---|
Expires | 1.HTTP1.0产物,可以在HTTP1.0和HTTP1.1中使用,简单易用 2.以时刻标识失效时间 |
1.时间是由服务器发送的(UTC),如果服务器时间和客户端事件存在不一致,可能出现问题 2.存在版本问题,到期之前的修改客户端是不可知的 |
Cache-Control | 1.HTTP1.1的产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题 2.比Expires多了很多选项设置 |
1.HTTP1.1才有的内容,不适用于HTTP1.0 2.存在版本问题,到期之前的修改客户端是不可知的 |
Last-Modified | 1.不存在版本问题,每次请求都会去服务器进行校验,服务器对比最后修改时间,如果相同则返回304,否则返回200以及资源内容 | 1.只要资源修改,无论内容是否发生实质性的变化, 都会将该资源返回给客户端 2.以时刻作为标识,无法识别一秒内进行多次修改的情况 3.某些服务器不能精确的得到文件最后修改时间 |
ETag | 1.可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况 2.不存在版本问题,每次请求都会去服务器进行校验 |
1.计算ETag值需要性能损耗 2.分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另一台服务器上进行验证时发现ETag不匹配的情况 |
用户刷新/访问行为
我们可以把刷新/访问界面的手段分成三类:
(1)在URL输入栏中输入然后回车/通过书签访问
(2)F5/点击工具栏中的刷新按钮/右键菜单重新加载
(3)Ctrl+F5
针对不同的刷新行为返回状态码总结:
from cache/304
Expires和Cache-Control都有一个问题就是服务端作为的修改,如果还在缓存时效里,那么客户端是不会去请求服务端资源的(非刷新),这就存在一个资源版本不符的问题。而强制刷新一定会发起HTTP请求并返回资源内容,无论该内容在这段时间内是否修改过;而Last-Modified和ETag每次请求资源都会发起请求,哪怕是很久都不会有修改的资源,都至少有一次请求响应的消耗。
对于所有可缓存资源,指定一个Expires或Cache-Control max-age以及一个Last-Modified或ETag至关重要。同时使用前者和后者可以很好的相互适应。前者不需要每次都发起一次请求来校验资源的时效性,后者保证当资源未出现修改的时候不需要重新发送该资源。而在用户的不同刷新页面行为中,二者的结合也能很好地利用HTTP缓存控制特性,无论在地址栏输入URL然后输入回车键进行访问,还是点击刷新,浏览器都能充分利用缓存内容,避免进行不必要的请求与数据传输。
避免304
把服务侧ETag的那套理论搬到前端来使用。页面的静态资源以版本形式发布,常用的方法是在文件名或参数带上一串md5或时间标记符。
结论:
/////////////////////////////////////////爱的分割线\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
啊,上面讲了那么多,都是HTTP相关,下面来总结一下各种类型的缓存吧!
正如本文开头所说,缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。
各种类型的缓存
根据是否需要重新向服务器发起请求,可以把HTTP缓存分为:强制缓存(也叫本地缓存,强缓存)和协商缓存(也叫对比缓存,比较缓存)。
1)浏览器加载资源时,先根据这个资源的一些http header判断它是否命中强制缓存(Cache-Control(优先级高),Expires),强制缓存如果命中,浏览器直接从自己的缓存中读取资源,不会发送请求到服务器。
2)当强制缓存没有命中(缓存过期)时,浏览器一定会发送一个请求到服务器,通过服务器端依据资源的另外一些http header(Last-Modified/If-Modified-Since,ETag/If-None-Match(优先级高))验证这个资源是否命中协商缓存,如果协商缓存命中(服务器返回状态码304),服务器会将这个请求返回,但是不会返回这个资源的数据,而是告诉客户端可以直接从缓存中加载这个资源,于是浏览器就会从自己的缓存中加载这个资源
3)强制缓存与协商缓存的共同点是:如果命中,都是从客户端中加载资源,而不是从服务器加载资源数据;区别是:强制缓存不发请求到服务器,协商缓存会发请求到服务器。
4)当协商缓存也没有命中(服务器返回状态码200 OK)时,浏览器直接从服务器加载资源数据。
浏览器第一次请求时:
浏览器再次请求时:
协商缓存
顾名思义,就是需要进行比较判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识和数据一起返回给客户端,客户端将二者备份到缓存数据库中。再次请求数据时,客户端将备份的缓存标识(Last-Modified/If-Modified-Since,ETag/If-None-Match(优先级高于Last-Modified))发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据。
强制缓存
服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存。不在时间内,进行协商缓存策略。
///////////////////////////////////////////////////////////////////////////////////////////////
另外,根据缓存是否能够被多个用户使用,可归为两类:私有缓存和共享缓存。共享缓存存储的响应能够被多个用户使用。私有缓存只能用于单独用户。
其他分类:
1.数据库缓存
memcached是一种数据库层面的缓存方案。
数据库缓存是指,当web应用的关系比较复杂,数据库中的表很多的时候,如果频繁地进行数据库查询,很容易导致数据库不堪重负。为了提供查询的性能,将查询后的数据放到内存中进行缓存,下次查询可以直接从内存中返回,提供响应效率。
2.CDN缓存
CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能。通常情况下,浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。从浏览器角度来看,整个CDN就是一个源服务器,从这个层面来说,浏览器和服务器之间的缓存机制,在这种架构下同样适用
3.代理服务器缓存
代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大。
4.浏览器缓存
每个浏览器都实现了 HTTP 缓存,我们通过浏览器使用HTTP协议与服务器交互的时候,浏览器就会根据一套与服务器约定的规则进行缓存工作。
5.应用层缓存
应用层缓存是指我们在代码层面上做的缓存。通过代码逻辑,把曾经请求过的数据或资源等,缓存起来,再次需要数据时通过逻辑上的处理选择可用的缓存的数据。
//////////////////////////////////////////////////////////////////////////////////
写在最后,本文照搬了多篇博主的文章,望海涵!