缓存是一个很大的话题,毕竟缓存作为一种性能优化的常规手段,在许多地方都有应用到。本文主要讨论web领域下浏览器与代理缓存。
浏览器缓存级别
L1: service worker
L2: memory cache
L3: disk cache
L4: push cache
L5: http request
L5-1: if-modified-since
L5-2: if-none-match
L5-3: max-age
L5-4: expires
从L1至L5,每一个层级都像一个漏斗,筛出那些可以从缓存中返回数据的请求,通过这种方式,可以极大减轻服务端的压力,同时也提升了响应速度。
memory cache & disk cache
这两种缓存方式,由浏览器自己全权管理。至于浏览器具体如何来管理的,我在网上没有找到对应的资料,只找到了二者的特性与区别。
- memory disk的存储位置是RAM(random access memory) 读取速度更快,但是容量小而且容易被清掉,关掉页面就请掉了;
- disk cache的存储位置是硬盘,读取速度慢一点,但是容量更大生效时间更长。
因此,我推断推断将size 小,需要频繁读取的文件放在memorydisk;而将size大且不常变化的放在disk memory。
我们知道,不管是什么缓存位置,容量不可能是无限大的,所以说不可避免地需要清除掉一些文件。对于disk cache,除了浏览器自身可能存在一些回收策略,作为浏览器用户也可以手动清除,右键刷新按钮,点击“Empty cache and hard reload”,这张页面的disk cache内容就消失了。
expires in response entity
这种缓存策略简单直接,试想以下过程:
- 源服务器告诉缓存服务器资源该资源可以用到什么时候(GMT,格林尼治时间)
- 客户端发请求的时候,请求头中带有Date(报文创建的时间日期,也是GMT格式),缓存服务器拿这个Date和原服务器给的Expires相比较,发现还在时间内, 就返回回去,如果发现超时了,则会问源服务器要最新的资源。
这里有一个尚未搞明白的问题:Date可以通过程序修改,可能造成时间比对失败,所以我认为这应该不允许通过程序修改才对?为什么要这样设计呢?
max-age in common header
max-age是cache-control的一个指令。cache-control是用来控制缓存策略的最重要的一个header。
image.png
为什么请求头和相应头中都包含max-age?且都代表相应的最大age值?
在客户端和源服务器之间存在一个缓存服务器。
- 如客户端请求头cache-control:max-age=5,代表它对缓存服务器说:只要这个文件自放你那儿时开始算起不超过5秒,那么直接把这个文件给我;
- 如果源服务器请求头cache-control: max-age=5,代表它对缓存服务器说:自我上次给你文件开始算起,5秒之内不需要验证是否过时,你直接用即可。
根据以上信息,可以推测出可能出现三种情况:
-
CASE1:客户端请求资源A时,客户端说5秒之内的文件可以,缓存服务器发现已经超过5秒了,于是去问源服务器要A,然而源服务器告诉缓存服务器,10秒之内都不用验证,你自己把缓存文件给客户端吧。
总之,客户端希望5秒过时,但仅仅只是希望而已秒,服务端才具有决定权,10秒之内的文件都会传送过去。这种情况下,不会有什么问题,因为按照服务端的要求给文件,文件是更新一点的。 -
CASE 2: 客户端说10秒之内的都可以,而服务端觉得不行,5秒就要过时,但这个时候客户端占据主动权,因为缓存服务器发现文件拿到8秒,依旧不会去问服务器而是直接给缓存。这种情况下服务器可以免责,因为是客户端自己提出要求说10秒之内都可以的。
-
CASE 3:服务端和客户端设置的超时时间一致,这种情况没有什么可以讨论的。
那么,什么情况下,客户端和服务端针对同一个资源所设置的max-age不一样呢?
经过我翻看多个网页的network面板,没有发现同时设置的,并且max-age全部都是在response header中设置,而且出现了很多max-age=31516000s,也就是 1 year。
仔细想想,哪怕一起设置也不会造成bug。服务端面向多个客户端,它不可能仅为某一个客户端来考虑,而是根据资源的更新频率来判断max-age设置为多少。客户端设置max-age时有两种情况:
- 比服务器的max-age更大,可能出于某种原因必须设置大一些,如果拿到了过期的,自己负责;
- 比服务器的max-age更小,这种情况不会有问题,只是找缓存服务器拿的更频繁。
同时可以推出,缓存服务器肯定记录了收到没个文件的时间,要不然它没有办法计算文件是否超时。
age in common head
首部字段 Age 能告知知客户端,源服务器在多久前创建了响应。
若创建该响应的服务器是缓存服务器,Age 值是指缓存后的响应再此发起认证到认证完成的时间值。
-----《HTTP图解》
age是用来标记资源的年龄的,也就是说从它诞生到现在,过去了多长时间。试想一下过程:
- 客户端请求A;
- 缓存服务器发现没有存,向服务器要;
- 服务器给了,并告诉缓存服务器5秒之内可以直接用(max-age。
接下来有两种情况:
- CASE1: 过了两秒客户端又来请求,缓存服务器发现中自己缓存了,并且age是2, 没有过时,所以缓存服务器直接返回了缓存文件,并且告诉客户端age是2;
- CASE2: 过了10秒,客户端又来请求,缓存服务器这是发现age=10>5,所以去问源服务器要最新的数据,源服务器返回了最新的文件给缓存服务器,缓存服务器将最新的文件给了客户端,并且将这份文件缓存起来,这时age=0。
这里我也有一个没搞明白的问题:客户端拿到这个age有什么用?用来判断本地缓存的文件有没有超时?可是我拿到age的时候,响应都拿到了还有什么意义?
no-store和no-cache有什么区别?
在缓存请求指令中:
- no-cache: 客户端将不会 接收缓存过的响应
- no-store: 不要缓存我,因为我可能包含机密信息
在缓存响应指令中:
- no-cache: 如果服务器返回的响应中包含 no-cache 指令,那么缓存服务器不能 对资源进行缓存。源服务器以后也将不再对缓存服务器请求中提出的资 源有效性进行确认,且禁止其对响应资源进行缓存操作
- no-store: 不要缓存我,因为我可能包含机密信息
从字面意思上很容易把no-cache误解成为不缓存,但事实上no-cache代表不 缓存过期的资源,缓存会向源服务器进行有效期确认后处理资源,也许称为 do-not-serve-from-cache-without-revalidation 更合适。no-store 才是真正地不进行缓存,请读者注意区别理解
----《HTTP图解》
last-modified in response entity & if-modified-since in request header
这套逻辑其实很简单,试想一下过程:
- 客户端发送请求,缓存服务器发现没有,去找原服务器;
- 原服务器返回了资源并且告诉请求方,last-mofied时间是GMTA;
- 客户端下次再请求这个资源的时候就会带上if-modified-since=GMAT,然后服务器通过检查GMTA之后文件有没有变化,便可以知道是否过时。
确实很简单,但是只能控制到秒级别,所以不够精确。
eTag in response header & if-none-match request header
eTag, 可以简单地理解为服务端给一个response编码了,得到了一个字符串,这个字符串可以唯一标示这个response。
if-none-match:
屏幕快照 2022-03-08 上午10.35.06.png
if-None-Match & if-Match
我在看网上的博客的时候,还看到一个if-Match头,不知道它和if-None-match的联系和区别,以及使用场景,于是我有开始查找资料。
屏幕快照 2022-03-08 上午10.41.49.png那么,什么情况下用if-match, 什么情况下用if-none-match?
If-Match is most often used with state-changing methods (e.g., POST,
PUT, DELETE) to prevent accidental overwrites when multiple user
agents might be acting in parallel on the same resource (i.e., to revent the "lost update" problem). It can also be used with safe
methods to abort a request if the selected representation does not
match one already stored (or partially stored) from a prior request.
发送产生副作用的请求,如UPDATE时,如果同时存在很多个客户端都在请求修改同一份数据,但是每个客户端都不知道有可能别人先于自己把数据改了,而正好修改这份数据时依赖改之前的数据的。
所以说带上if-match,保证只有在没改的情况下,才真正执行请求。
这时候用的是强e-tag.
If-None-Match is primarily used in conditional GET requests to enable
efficient updates of cached information with a minimum amount of
transaction overhead. When a client desires to update one or more
stored responses that have entity-tags, the client SHOULD generate an
If-None-Match header field containing a list of those entity-tags
when making a GET request; this allows recipient servers to send a
304 (Not Modified) response to indicate when one of those stored
responses matches the selected representation.
发送GET请求的时候,带上if-None-Match, 让服务端来判断文件有没有被变化。如果匹配不上则说明变化了,那么要返回最新的文件,否则命中缓存,返回304.
他们两可以同时出现在一个报文中吗?
可以同时出现,但是只有一个起作用,取决于Method是什么。
The If-Match header field can be ignored by caches and intermediaries
because it is not applicable to a stored response.
场景适配
变化频繁的用强制缓存,指定max-age,比如需要从数据库读取的请求;变化不频繁的用协商缓存,比如静态html.,js文件
工作中用到的缓存思想
closure
使用闭包缓存住一个response。
cachedFectch = (function(){
let cache;
return function(url, params){
if(!cache){
cache = fetch(url, params)
}
return cache;
}
})()
Rxjs shareReplay。
在Angular项目中,试过用shareReplay来缓存住一个response。
class Component{
data$ = this.dataService.getData().pipe(shareReplay(1));
getData(){
return this.data$
}
}
网友评论