缓存策略
一、缓存的种类
缓存的原理是在客户端首次请求后保存一份请求资源的响应副本存储在客户端中,当用户再次发起相同的请求后,如果判断缓存命中则拦截请求,将之前缓存的响应副本返回给用户,从而避免重新向服务器发起资源请求。
缓存的技术种类有很多,比如代理缓存,浏览器缓存,网关缓存,负载均衡器及内容分发网络等,大致可以分为两类,共享缓存和私有缓存。
共享缓存指的是缓存内容可以被多个用户使用,如公司内部架设的Web代理,私有缓存是只能单独被用户使用的缓存,如浏览器缓存。
HTTP缓存是前端开发中最常接触的缓存机制之一,他又可细分为强制缓存与协商缓存,二者最大的区别在于判断缓存命中时浏览器是否需要向服务器进行询问。
强制缓存不会去询问,协商缓存则仍旧需要询问服务器。
二、强制缓存
对于强制缓存而言,如果浏览器判断所请求的目标资源有效命中则可直接从强制缓存中返回请求的响应,无需与服务器进行任何通信。也就是说强制缓存是在客户端进行的,这样速度就会很快。
强制缓存相关的两个字段是expires
和cache-control
。缓存资源的请求状态码是 200 OK (from memory cache)
expires
expires
是在HTTP1.0
协议中声明的过期时间戳,由服务器指定并通过响应头返回浏览器,浏览器在收到带有该字段的响应体后进行缓存。
之后浏览器再发送相同的请求便会对比expires
与本地当前的时间戳,如果当前请求的本地时间戳小于expires
的值,则说明缓存还未过期,可以直接使用。
否则缓存过期重新向服务器发送请求获取响应体。
res.writeHEAD(200, {
Expires: new Date('2021-6-18 12: 51: 00').toUTCString(),
})
expires
存在一个很大的漏洞就是对本地时间戳过分依赖,如果客户端本地的时间与服务器时间不同步,或者客户端时间被修改,那么缓存过期的判断可能就无法和预期相符。
为了解决这个问题,HTTP1.1新增了cache-control
字段来对expires
的功能进行扩展和完善。
cache-control
cache-control
的值为maxage=xxx
来控制响应资源的有效期,xxx
是一个以秒
为单位的时间长度,表示该资源在被请求到的一段时间内有效,以此便可避免服务端和客户端时间戳不同步而造成的问题。
res.writeHEAD(200, {
'Cache-Control': 'maxage=1000',
})
除了max-age
还可以设置其他参数,比如下表
参数值 | 说明 |
---|---|
no-cache | 禁止强缓存,只进行协商缓存 |
no-store | 禁止强缓存和协商缓存,每次都获取最新 |
private | 只能被浏览器缓存 |
public | 即可被浏览器缓存又可以被代理服务器缓存,比如图片、字体库等通常不会改变的文件 |
s-maxage | 缓存在代理服务器上的过期时长,需要配合public 来使用 |
cache-control
可以设置多个值,使用逗号分割,但注意的是no-cache
/no-store
,private
/public
这两对属性是互斥的,不可以同时设置,例如
res.writeHEAD(200, {
'Cache-Control': 'no-cache,public, max-age=31600',
})
cache-control
能作为expires
的完全替代方案,目前expires只作为兼容使用。
三、协商缓存
last-modified/if-modified-since
协商缓存就是客户端请求资源的时候服务器会返回响应内容及内容的修改时间,修改时间存在last-modified
字段中。
缓存资源的请求状态码是 304 Not Modified
客户端在请求的时候如果客户端存储了last-modified
就将它的值放在if-modified-since
字段中发送到服务器。
服务器接收到请求后通过比对前端传过来的时间和资源的修改时间,如果二者相同则说明缓存未过期,就告诉浏览器直接使用缓存中的文件,如果过期了就返回对应文件并且将新的修改日期重新返回。
客户端继续缓存新的修改时间。
const http = require('http');
const fs = require('fs');
const url = require(''url');
http.creatServer((req, res) => {
const { pathname } = url.parse(req.url);
// 获取文件日期
fs.stat(`www/${pathname}`, (err, stat) => {
if (err) {
res.writeHeader(404);
res.write('Not Found');
res.end();
} else {
if (req.headers['if-modified-since']) {
const oDate = new Date(req.headers['if-modified-since']);
const time_client = Math.floor(oDate.getTime() / 1000);
const time_server = Math.floor(stat.mtime.getTime() / 1000);
if (time_server > time_client) { // 服务器的文件时间大于客户端
sendFileToClient();
} else {
res.writeHeader(304);
res.write('Not Modified');
res.end();
}
} else {
sendFileToClient();
}
function sendFileToClient() {
let rs = fs.createReadStream(`www/${pathname}`);
res.setHeader('Last-Modifyed', state.mtime.toGMTString());
rs.pipe(res);
rs.on('error', err => {
res.writeHeader(404);
res.write('Not Found');
res.end();
})
}
}
})
}).listen(8080);
ETag/If-None-Match
上面的这种缓存方式存在两个问题
只是根据资源最后的修改时间戳进行判断,如果文件没有变更只是保存了一下修改时间也会变化。
标识时间是秒,如果修改特别快在毫秒内完成(程序修改会有这样的速度),那么就无法识别缓存过期。
为了解决这个问题从HTTP1.1
规范开始新增了一个ETag
的头信息。
其内容主要是服务器为不同资源进行哈希运算生成的一个字符串,该字符串类似于文件指纹,只要文件内容编码存在差异,对应的ETag
标签值就会不同,因此可以使用ETag
对文件资源进行更精准的变化感知。
const etag = require('etag')
res.setHeader('etag', etag(data));
基于ETag
发送的请求会在请求头中以If-None-Match
传递给服务器。
在协商缓存中ETag
并非last-modified
的替代方案而是一种补充方案,因为服务器生成ETag需要付出额外的计算开销,恰当的方式是根据具体的资源使用场景选择恰当的缓存校验方式
四、缓存策略
如果不考虑客户端缓存容量和服务器计算能力的理想情况,我们当然希望客户端浏览器上的缓存触发率尽可能高,留存时间尽可能长,同时还要ETag实现当资源更新时进行高效的重新验证。
但实际情况往往是容量和计算能力都有限,因此就需要指定合适的缓存策略,利用有效的资源达到最优的性能效果。
明确需求边界,力求在边界内做到最好。
在使用缓存技术优化性能的过程中,有一个问题是不可逾越的,我们既希望缓存能在客户端尽可能长久的保存,又希望他能在资源发生修改时进行及时更新。这是两个互斥的需求。
如何兼顾二者呢?
可以将网站所需要的资源按照不同的类型去拆解,为不同类型的资源制定相应的缓存策略。
HTML
首先html文件是包含其他文件的主文件,为保证当其发生改变能及时更新,应该将其设置为协商缓存。
cache-control: no-cache
图片
图片文件的修改基本都是替换,同时考虑图片文件的数量及大小可能对客户端缓存空间造成不小的开销,所以可以采用强制缓存且过期时间不宜过长。
cache-control: max-age=86400
CSS
css样式表属于文本文件,可能存在的内容不定期修改,还想使用强制缓存来提高重用效率,故可以考虑在样式表文件的命名中增加指纹或版本号(一般为hash
值),这样发生修改后不同的文件便会有不同的文件指纹,也就是请求的url不同。css的缓存时间可以设置长一些, 一年。
cache-control: max-age=31536000
JS
js脚本文件可以类似CSS
的设置,采用指纹和较长的过期时间,如果js中包含了用户的私人信息而不想让中间代理缓存,可添加private
属性。
cache-control: private, max-age=31536000
缓存策略就是为不同的资源进行组合使用强制缓存,协商缓存及文件指纹或版本号,这样可以做到一举多得,及时修改更新,较长缓存过期时间及控制所能进行缓存的位置。
不存在适用于所有场景下的最佳缓存策略,凡是恰当的缓存策略都需要根据具体的场景考虑制定。
其他
- 拆分源码,分包加载:按模块打包多个单独的文件,每次修改后,仅需拉取发生改变的模块代码包
- 预估资源的缓存时效:根据不同资源的不同需求特点来规划响应的缓存更新失效
- 控制中间代理的缓存:凡是涉及用户隐私信息的尽量避免中间代理的缓存
- 避免网址的冗余:要将相同的资源设置为不同的URL。这会导致缓存失效
五、CDN缓存
定义
CND
即Content Delivery Network
,内容分发网络,使用户在请求所需访问的内容时能够就近获取,工作原理就是就近响应,如果我们将资源放置在CDN
上,当海南的用户访问网站时,资源请求首先进行DNS
解析,这个时候DNS
会询问CDN
服务器有没有就近的服务器,如果有就链接就近服务的IP
地址获取资源。
缓存过程
由于~服务器将CDN
的域名解析权交给了CNAME
指向的专用DNS
服务器,所以用户输入域名的解析最终是在CDN
专用的DNS
服务器上完成的。
解析出的IP
地址并非确定的CDN
缓存服务器地址,而是CDN
负载均衡器的地址。
浏览器会重新向该负载均衡器发起请求,经过对用户IP地址的距离,所请求资源内容的位置及各个服务器状态的综合计算,返回给用户确定的缓存服务器IP地址。
如果这个过程发生所需资源未找到的情况,那么此时便会依次向上一级缓存服务器继续请求查询,直至追溯到网站所在的跟服务器并将资源拉取到本地进行缓存。
虽然这个过程看起来稍微复杂一些,但是用户是无感知的,并且能带来比较明显的资源加载速度的提升,因此对目前所有一线互联网产品来说,使用CDN已经不再是一条建议而是规定。
适用范围
CDN
主要针对的是静态资源而非适用网站所有的资源类型。所谓静态资源就是不需要业务服务器参与计算的资源,比如第三方的库,js脚本文件,css样式文件,图片等。
如果是动态资源比如依赖服务端渲染的html
就不适合放在CDN
上。
核心功能
CDN
网络的核心功能包括两点,缓存与回源,
- 缓存:将所需的静态资源文件复制一份到
CDN
缓存服务器上 - 回源:如果未在
CDN
缓存服务器上查找到目标资源或者资源过期,则重新追溯到网站根服务器获取相关资源。
优化
自身的性能优化,静态资源边缘化,域名合并优化和多级缓存架构优化。这些可能需要前后端一起配合完成。
域名区分
一般情况CDN
会和主站域名区分,这样的好处是避免静态资源请求携带不必要的cookie
信息,还有就是考虑浏览器对同一域名下并发请求的限制。
- 同一域名下的所有请求都会携带全部
cookie
信息,如果所有资源都放在主站域名下,所有的请求全部携带cookie
数据量也是很大的 - 浏览器对于同域名下的并发请求存在限制,通常
Chrome
的并发限制是6
,可以通过增加类似域名的方式来提高并发请求数
当然这种方式对缓存命中是不友好的,如果并发请求了相同的资源使用了不同的域名,那么之前的缓存就失去了意义