最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。
浏览器输入url之后,就会进行下面一系列判断,来实现页面渲染。
首先讲一下常见的http缓存~
HTTP缓存常见的有两类:
- 强缓存:可以由这两个字段其中一个决定
-
- expires
- cache-control(优先级更高)
- 协商缓存:可以由这两对字段中的一对决定
-
- Last-Modified,If-Modified-Since
- Etag,If-None-Match(优先级更高)
强缓存
使用的是express框架
expires
app.get('/login', function(req, res){
// 设置 Expires 响应头
const time = new Date(Date.now() + 300000).toUTCString()
res.header('Expires', time)
res.render('login');
});
然后我们在前端页面刷新,我们可以看到请求的资源的响应头里多了一个expires的字段, 取消Disable cache
刷新
勾选Disable cache
但是,Expires已经被废弃了。对于强缓存来说,Expires已经不是实现强缓存的首选。
因为Expires判断强缓存是否过期的机制是:获取本地时间戳,并对先前拿到的资源文件中的Expires字段的时间做比较。来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:“如果我本地时间不准咋办?”
是的,Expires过度依赖本地时间,如果本地与服务器时间不同步,就会出现资源无法被缓存或者资源永远被缓存的情况。所以,Expires字段几乎不被使用了。现在的项目中,我们并不推荐使用Expires,强缓存功能通常使用cache-control字段来代替Expires字段。
cache-control
其实cache-control跟expires效果差不多,只不过这两个字段设置的值不一样而已,前者设置的是秒数,后者设置的是毫秒数
app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'max-age=300')
res.render('login');
});
前端页面响应头多了cache-control这个字段,且300s内都走本地缓存,不会去请求服务端
Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。
Cache-control中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Expires所存在的巨大漏洞。
Cache-control的多种属性:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control
但是使用最多的就是no-cache和no-store,接下来就重点学习这两种
no-cache和no-store
no_cache是Cache-control的一个属性。它并不像字面意思一样禁止缓存,实际上,no-cache的意思是强制进行协商缓存。如果某一资源的Cache-control中设置了no-cache,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存。而no-store就是禁止所有的缓存策略了。
app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});
no-cache(进行协商缓存,下次再次请求,没有勾选控制台Disable cache,状态码是304)
app.get('/login', function(req, res){
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-store')
res.render('login');
});
no-store(每次都请求服务器的最新资源,没有缓存策略)
强制缓存就是以上这两种方法了。现在我们回过头来聊聊,Expires难道就一点用都没有了吗?也不是,虽然Cache-control是Expires的完全替代品,但是如果要考虑向下兼容的话,在Cache-control不支持的时候,还是要使用Expires,这也是我们当前使用的这个属性的唯一理由。
协商缓存
与强缓存不同的是,强缓存是在时效时间内,不走服务端,只走本地缓存;而协商缓存是要走服务端的,如果请求某个资源,去请求服务端时,发现命中缓存则返回304,否则则返回所请求的资源,那怎么才算命中缓存呢?接下来讲讲
Last-Modified,If-Modified-Since
简单来说就是:
- 第一次请求资源时,服务端会把所请求的资源的最后一次修改时间当成响应头中Last-Modified的值发到浏览器并在浏览器存起来
- 第二次请求资源时,浏览器会把刚刚存储的时间当成请求头中If-Modified-Since的值,传到服务端,服务端拿到这个时间跟所请求的资源的最后修改时间进行比对
- 比对结果如果两个时间相同,则说明此资源没修改过,那就是命中缓存,那就返回304,如果不相同,则说明此资源修改过了,则没命中缓存,则返回修改过后的新资源
基于last-modified的协商缓存实现方式是:
- 首先需要在服务器端读出文件修改时间,
- 将读出来的修改时间赋给响应头的last-modified字段。
- 最后设置Cache-control:no-cache
三步缺一不可。
app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
console.log(mtime.toUTCString(), '--------')
// 响应头的last-modified字段
res.header('last-modified', mtime.toUTCString())
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});
当index.css发生改变再次请求时
终端输出的时间变化
服务端的时间跟last-modified的值是一致的
使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。
1.因为是更具文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。
2.当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件。
为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)
Etag,If-None-Match
ETag就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。
其实Etag,If-None-Match跟Last-Modified,If-Modified-Since大体一样,区别在于:
- 后者是对比资源最后一次修改时间,来确定资源是否修改了
- 前者是对比资源内容,来确定资源是否修改
那我们要怎么比对资源内容呢?我们只需要读取资源内容,转成hash值,前后进行比对就行了!
app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
// const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
// console.log(mtime.toUTCString(), '--------')
// 响应头的last-modified字段
// res.header('last-modified', mtime.toUTCString())
// 设置ETag
const ifNoneMatch = req.header['if-none-match']
const hash = crypto.createHash('md5')
const fileBuf = fs.readFileSync(path.join(__dirname, 'public/index.css'))
hash.update(fileBuf, 'utf8')
const etag = `"${hash.digest('hex')}"`
console.log(etag, '---etag----')
// 对比hash值
if (ifNoneMatch === etag) {
res.status = 304
} else {
res.header('etag', etag)
// ctx.body = fileBuffer
}
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});
当资源发生改变时,状态码变成200,更新缓存
比如更改css样式
ETag也有缺点
- ETag需要计算文件指纹这样意味着,服务端需要更多的计算开销。如果文件尺寸大,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能。显然,ETag在这样的场景下就不是很适合。
- ETag有强验证和弱验证,所谓将强验证,ETag生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。ETag还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高。会降低协商缓存的有效性。
值得注意的一点是,不同于cache-control是expires的完全替代方案(说人话:能用cache-control就不要用expiress)。ETag并不是last-modified的完全替代方案。而是last-modified的补充方案(说人话:项目中到底是用ETag还是last-modified完全取决于业务场景,这两个没有谁更好谁更坏)。
disk cache & memory cache
磁盘缓存+内存缓存,这两种缓存不属于http缓存,而是本地缓存了~
我们直接打开掘金官网,点击network,类型选择all
可以看的很多请求,这里请求包括了静态资源+接口请求
这里我们能够看的很多请求的size中有很多是disk cache(磁盘缓存)
也有一些图片是memory cache(内存缓存)
这两者有什么区别呢?
disk cache: 磁盘缓存,很明显将内容存储在计算机硬盘中,很明显,这种缓存可以占用比较大的空间,但是由于是读取硬盘,所以速度低于内存
memory cache: 内存缓存,速度快,优先级高,但是大小受限于计算机的内存大小,很大的资源还是缓存到硬盘中
上面的浏览器缓存已经有三个大点了,那它们的优先级是什么样的呢?
缓存的获取顺序如下:
1.内存缓存
2.磁盘缓存
3.强缓存
4.协商缓存
如果勾选了Disable cache,那磁盘缓存都不存在了,之还有内存缓存
我还发现,勾选了Disable cache,就base64图片一定会在内存缓存中,其他图片则会发起请求;而不勾选了Disable cache,则大多数图片都在内存缓存中
CDN缓存
CDN缓存是一种服务端缓存,CDN服务商可以将源站上的资源缓到其各地的边缘服务器节点上。当用户访问该资源时,CDN再通过负载均衡将用户的请求调度到最近的缓存节点上,有效减少了链路回源,提高了资源访问效率及可用性,降低带宽消耗。
如果客户端没有命中缓存,那接下来就要发起一次网络请求,根据网络环境,一般大型站点都会配置CDN,CDN会找一个最合适的服务节点接管网络请求。CDN节点都会在本地缓存静态文件数据,一旦命中直接返回,不会穿透去请求应用服务器。并且CDN会通过在不同的网络,策略性地通过部署边缘服务器和应用大量的技术和算法,把用户的请求指定到最佳响应节点上。所以会减少非常多的网络开销和响应延迟。
如果没有部署CDN或者CDN没有命中,请求最终才会落入应用服务器,现在的http服务器都会添加一层反向代理,例如nginx,在这一层同样会添加缓存层,代表技术是squid,varnish,当然nginx作为http服务器而言也支持静态文件访问和本地缓存技术,当然也可以使用远程缓存,如redis,memcache,这里缓存的内容一般为静态文件或者由服务器已经生成好的动态页面,在返回用户之前缓存。
如果前面的缓存机制全部失效,请求才会落入真正的服务器节点。
总结
1.如果页面是协商缓存,如何获取页面最新内容?
协商缓存比较好办,那就刷新页面,不过需要勾选Disable cache,但是用户不知道打开控制台怎么办?
那就右击页面的刷新按钮,然后选择硬性重新加载,或者清空缓存并硬性重新加载,页面就获取到最新资源了
2.如果页面没有设置cache-control,那默认的缓存机制是什么样的?
默认是协商缓存,这也符合浏览器设计,缓存可以减少宽度流量,加快响应速度
3.如果项目重新部署还是没有更新,怎么办?
在确定项目已经部署成功
这样子,可以去问一下公司的运维同事,这个项目是否有CDN缓存
如果项目的域名做了CDN缓存,就需要刷新CDN目录,来解决缓存问题了,不然就只能等,等CDN策略失效,来请求最新的内容
向如下配置的缓存策略,只有过30天才会去真正服务器去请求最新内容
当然你可以测试一下是否为CDN缓存,在url后面拼接一个参数,就能够获取到最新资源了,比如有缓存的链接是https://baidu.com/abc
你可以在浏览器中输入https://baidu.com/abc&t=1234来判断一下,如果访问的是更改后的内容了,那就是原来的链接有CDN缓存导致没有刷新
当然特定场景,我们不能随意给链接后面添加参数,所以这也只适用于测试一下是否有CDN缓存
所以最好的解决办法还是需要让运维同事去刷新目录,这样就能快速解决CDN缓存问题。
参考链接
https://juejin.cn/post/7127194919235485733
https://juejin.cn/post/7177568033316012088
https://xiaolincoding.com/network/2_http/http_interview.html#http-%E7%BC%93%E5%AD%98%E6%8A%80%E6%9C%AF