说说缓存

189 阅读7分钟

突然发现本菜鸡已经好久好久没发文了,正好最近在准备梳理一下知识体系,所以趁机发个文记录一下。大佬请绕道QAQ。

你还记得你的高中同学李华吗?没错,就是那个衬衫的价格是九磅十五便士的李华同学,她已经大学毕业啦。

李华毕业之后顺利成为了一个前端切图仔,这天她去面试,遇到了一个面试官,面试官很帅很帅(请自行脑补大碗宽面),李华的心底很凉很凉。

面试官说:请问浏览器的缓存策略是什么?

李华:浏览器的缓存策略当然是分为强缓存和协商缓存了,强缓存是缓存命中时获取本地磁盘的数据,并不会真正的请求服务端,而协商缓存是每次都会请求到服务端,由服务端决定是否使用缓存。

面试官:强缓存能不能仔细说说?

李华:强缓存和协商缓存其实都需要设置请求头来实现,只不过请求头和请求头的值不一样罢了。

强缓存有两种请求头的设置方法:

1、只设置expires

2、通过cache-control来实现。

面试官:继续。

李华斜眼看了一眼面试官,继续说道:expires设置的是系统的绝对时间(可以理解为电脑的时间),这种会有问题,假如我修改了电脑的时间,不从互联网获取,缓存很可能会失效。比如我的服务端设置缓存是到23年7月21日,今天是23年7月20日,正常情况下我的请求是可以走缓存的,这里的请求并不单单只是静态文件如图片等,接口也是可以设置缓存的,只是在开发业务是很少很少这么干而已。但是!今天下午我表弟来了,用我的电脑打荒野大镖客2,手误把我电脑时间改成了23年7月22日,这时再去请求资源就不会走缓存了。

判断强缓存最简单的方式是看浏览器的network,如果是缓存的话,在size下会显示memory cache或者是disk cache,前者是从内存中获取,后者是从磁盘中获取。当前页签关闭之后缓存并不会受影响,整个浏览器关闭之后缓存会从内存转移到磁盘中(至少chrome,memory cache会变为disk cache)。

如果通过cache-control来设置的话就会有多种情况,再瞟了一眼之后李华继续说道:通过cache-control通常使用最多的是max-age,即缓存过期时间,单位是秒,意思是多少秒之后缓存会过期,这段时间都会命中缓存。

cache-control多种情况其实是可以设置多个不同的值:

max-age是强缓存的过期时间,单位是秒

s-maxage是代理服务器的缓存过期时间

no-store是不设置缓存,即不使用强缓存也不使用协商缓存

no-cache:强制使用协商缓存

private/public:如果cache-control不设置的话默认值就是private,即只可以被浏览器缓存,代理服务器不允许缓存,而public正好相反,是代理和浏览器都可以缓存。

我们可以使用多个不同的值来组合,例如:

Cache-control: public, max-age=1000, s-maxage=10000

李华心想:死鬼,我回答这么好还不迷死你。

面试官:不错,你说说如果 cache-control设置max-age=3会是什么情况?

李华:人蛮王都能硬5秒呢,你3秒就不行了,啧啧啧(内心意淫)。

李华清了清嗓子,道:如果设置max-age=3的话,会是这样的情况:首先请求后端不走缓存,然后在接下来的3秒内如果还会请求这个资源,则会命中缓存,如果是在3秒后请求,则仍然不会命中缓存,所以这种短时间的缓存并没有用,一边还没开始,另一边已经结束了。

面试官:你不要话里有话,说说协商缓存吧。

李华心虚的低下了头,缓了一会说道:协商缓存也有两种方式,这两种方式都需要先设置cache-control为no-cache

1、if-modified-since和last-modified

2、etag 和 if-none-match

首先如果用第一种的话,服务端只需要在请求头上添加last-modified,值和expires一样是一个绝对时间,浏览器后续再有这个请求的话,会在头上携带if-modified-since,值是last-modified的值,服务端会根据if-modified-since和last-modified来判断是否需要重新请求资源,如果两个字段时间不一致则会重新请求资源的同时设置last-modified的值,否则的话就会命中缓存。

但是这种情况会有一个弊端,如果服务端资源在频繁的变化,这种方式可能就会检测不到,因为last-modified只会识别秒级的修改,对于毫秒级的修改是检测不到的。

协商缓存还可以使用另一种方式,即etag的方式。etag即是服务端会根据特殊的etag算法对每个资源文件的内容来生成一个字符串,同时在请求头上设置etag字段,前端后续的请求会携带etag的值。例如服务端生成的etag值为"大碗宽面"(开个玩笑)请求的response headers中会携带etag=大碗宽面,而后续前端的请求头中会携带If-None-Match:大碗宽面。服务端根据If-None-Match和自己生成的etag值来判断文件是否有更新。这种方式与比if-modified-since相比,可以在文件内容频繁更改场景下最大程度的保证文件内容的正确性,但理所应当的会消耗较大的服务端资源,因为每次请求都会重新读取文件并生成etag。

另一方面,etag还分为强验证和弱验证,强验证会消耗更多的服务端资源,但是生成的etag会更加准确,能够保证每个字节都相同,而弱验证则相反,生成的etag值并不能保证每个字节都准确,只能保证大致准确,而优点则是消耗比较低。所以我们应该根据不同的场景选择不同的缓存策略。

面试官:很不错,协商缓存和强缓存相比有什么区别?

李华:与强缓存不同的是,协商缓存命中之后拿到的状态码即status是304,而不是强缓存的200,强缓存如果没有命中缓存的话请求是到不了服务端的,协商缓存即使命中了缓存,也会请求到服务端,但是服务端不会再去重新发送资源给客户端,这是协商缓存和强缓存第二个不同点。

面试官:给你一张纸,能把上面说的if-modified-since只识别秒级的修改复现一下吗?

李华心想:真抠,什么时代了还手写代码,至少给个电脑啊。

李华花了五分钟时间写了一个demo:

其实就是一个借口请求后端,后端读取文件内容然后将文件内容返回前端,但同时启动一个新程序不断修改这个文件。

首先用node写一个服务端程序:

const http = require('http');
const server = http.createServer((req, res) => {})
server.listen(3000, () => {
  console.log('服务端已启动')
})

另外单独对接口逻辑进行处理:

// 协商缓存
    const { mtime } = fs.statSync(path.resolve(__dirname, './text.txt'))
    const IfModifiedSince = req.headers['if-modified-since']
    if (IfModifiedSince === mtime.toUTCString()) {
      const data = fs.readFileSync(path.resolve(__dirname, './text.txt'), 'utf-8').toString()
      console.log(data, '请求进来时的最新文件内容')
      // 缓存生效
      res.statusCode = 304
      res.end()
      return
    }
    // 缓存不生效重新设置last-modified
    const data = fs.readFileSync(path.resolve(__dirname, './text.txt'), 'utf-8').toString()
    res.setHeader('last-modified', mtime.toUTCString())
    res.setHeader('cache-control', 'no-cache')
    res.setHeader('Content-type', 'application/json;charset=utf-8;')
    const str = JSON.stringify({ data })
    res.end(str)

即使是这样还是不能模拟文件频繁被修改的场景,我们在写一个程序:

const fs = require('fs');
const path = require('path');
let i = 0
setInterval(() => {
  fs.writeFileSync(path.resolve(__dirname, './text.txt'), `修改第${i}次`, 'utf-8')
  i++
}, 10)

这个程序一直在修改text.txt的内容,而接口直接读取的也是这个文件,运行之后可以清除的看到拿到的并不是最新的内容。

面试官:嗯,很不错,本次面试通过,请等待下次面试通知。

文中代码链接请点=>这里