什么是缓存?
缓存(cache),这个词来源于计算机硬件,由于 CPU 运算速度极快,而内存和硬盘的读写速度相对来说又特别慢,所以 CPU 每次有数据存取需要的时候,就要等待磁盘的缓慢操作,这对于 CPU 的性能是极大的浪费,所以为了快速响应 CPU 需求,需要有一个快速的存储设备来临时存放数据,这便是缓存。
百度百科 缓存 的解释:
定义:原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用 DRAM 技术,而使用昂贵但较快速的 SRAM 技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一。
原理:缓存的工作原理是当 CPU 要读取一个数据时,首先从 CPU 缓存中查找,找到就立即读取并送给 CPU处理;没有找到,就从速率相对较慢的内存中读取并送给 CPU 处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。正是这样的读取机制使CPU读取缓存的命中率非常高(大多数 CPU 可达90%左右),也就是说 CPU 下一次要读取的数据90%都在 CPU 缓存中,只有大约10%需要从内存读取。这大大节省了 CPU 直接读取内存的时间,也使 CPU 读取数据时基本无需等待。总的来说,CPU 读取数据的顺序是先缓存后内存。
什么是Web缓存?
想要加快浏览器加载网络资源的速度,有以下方法:
- 减少响应内容大小:如使用
HTTP/2的压缩头部功能和gzip算法压缩响应体内容 - 使用「缓存」:把一些资源存储下来,从而无需再次加载
Web缓存 是用于临时存储(缓存)Web 文档(如 HTML 页面和图像),以减少服务器延迟的一种信息技术。Web 缓存系统会保存下通过这套系统的文档的副本;如果满足某些条件,则可以由缓存满足后续请求。使用缓存要尽可能地让浏览器从缓存中获取资源,并保证被使用的缓存与服务端最新的资源保持一致,一般只缓存静态资源。
解决问题:
- 减少不必要的网络传输、节约宽带
- 减少服务器负载,避免服务器过载的情况出现
- 更快的加载页面
缺点:
- 占内存(有些缓存会被存到内存里)
WEB缓存的分类
数据库缓存
当web应用关系复杂,数据表增多时,可以将查询后的数据放到内存中进行缓存。下次再查询时,就直接从内存缓存中获取,从而提高响应速度。
CDN缓存
当我们发送一个web请求时,CDN会帮我们计算去哪得到这些内容的路径短且快。
浏览器缓存
每个浏览器都实现了 HTTP 缓存,我们通过浏览器使用 HTTP 协议于服务端进行交互的时候,浏览器就会根据一套一服务端约定的规则进行缓存工作,当我们在浏览器中点击“前进”和“后退”按钮时,利用的就是浏览器的缓存机制。
代理服务器缓存
跟浏览器缓存的性质类似,但是代理服务器缓存面向的群体更广,规模更大。同一个副本会被重用多次,因此在减少响应时间和带宽使用方面更有效。
按服务于用户角度分类
单一用户 · 私有缓存
针对专有用户,web浏览器中内建的私有缓存,在本地电脑的磁盘( Disk Cache )和内存( Memory Cache )中存放缓存内容。
多个用户 · 公有缓存
有代理服务器缓存、CDN缓存等。共享代理服务器、或者是缓存代理服务器、或者代理缓存。基本思想是:在靠近客户单的地方使用小型缓存服务器,更高层次中,逐级采用更大、功能更强的缓存服务器装载更多用户共享的资源。
浏览器缓存的位置
浏览器缓存通过 HTTP/HTTPS 实现,存储位置有四种:
Service WorkerMemory Cache(内存缓存)Disk Cache(硬盘缓存)Push Cache(推送缓存)
以上缓存全部没有命中就会进行网络请求。
Service Worker
Service Worker 是运行在浏览器背后的独立线程,可以用来实现缓存功能。使用 Service Worker 的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
Memory Cache
Memory Cache 是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据高效,但是缓存持续性很短。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。而且由于计算机中的内存比硬盘容量小得多,我们能够使用存储缓存的内存并不多。
内存缓存在缓存资源时并不关心返回资源的 HTTP 缓存头 Cache-Control,同时资源的匹配也并非仅仅是对 URL 做匹配,还可能会对 Content-Type,CORS 等其他特征做校验。
Disk Cache
Disk Cache 是存储在硬盘中的缓存,读取速度比 Memory Cache 慢,但是存储量更大。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。
Push Cache
Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
浏览器什么时候会把缓存存储到内存中,什么时候存储到硬盘中呢?一般来说:
- 小文件优先存储到内存中,反之存储到硬盘中
- 使用频率高的缓存到硬盘中
控制缓存的基本机制
HTTP定义了三种控制缓存的基本机制:新鲜度,验证和失效。
新鲜度
允许在不在源服务器上重新检查的情况下使用一个响应,并且可以由服务器和客户端来控制。例如,Expires 响应头给出文档过期的日期,而 Cache-Control: max-age=N指示告诉缓存该响应在多少秒内保持新鲜。
验证
可用于检查缓存的响应是否过时之后仍然有效。例如,若响应有一个 Last-Modified 头,缓存可以使用 If-Modified-Since 头来发出一个条件请求,来查看它是否已经改变。ETag(实体标签)机制还允许强弱验证。
失效
通常是另一个请求通过缓存的一个结果。例如,如果与缓存的响应关联的URL随后获得POST、PUT 或 DELETE 请求,则缓存的响应将失效。
许多CDN和网络设备制造商已经用动态缓存取代了这个标准的HTTP缓存控制。
本地缓存
localStorage、sessionStorage、cookie 缓存一些必要的数据,比如用户信息。
Cookie
Cookie 是一段不超过 4KB 的小型文本数据,由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成。
Web Storage
HTML5 提供的解决方案,由两部分组成:sessionStorage 与 localStorage。
- localStorage: 用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
- sessionStorage:用于本地存储一个会话(
session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。
区别
- 存储大小限制:
cookie不超过4KBWeb Storage可达到5MB或更大。
cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。而Web Storage不会自动把数据发送给服务器,仅在本地保存。- 数据有效期不同:
cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭。sessionStorage:仅在当前浏览器窗口关闭之前有效。localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据。
- 作用域不同:
cookie、localstorage:在所有同源窗口中共享。sessionStorage:不在不同的浏览器窗口中共享,即使是同一个页面。
Web Storage支持事件通知机制,可以将数据更新的通知发送给监听者。
http缓存
根据是否需要向服务器重新发起 HTTP 请求,将缓存过程分为两个部分:
- 强制缓存
- 协商缓存
两种缓存方式最终使用的都是本地缓存;前者无需与服务器交互,后者需要。
强制缓存
如果浏览器判断请求的目标资源有效命中强缓存,如果命中,则可以直接从内存中读取目标资源,无需与服务器做任何通讯
强缓存会把资源放到 memory cache 和 disk cache 中。具体操作浏览器自动分配,看谁的资源利用率不高就分给谁。
disk cache:存储图像和网页等资源memory cache:大部分操作系统缓存文件等资源
查找浏览器缓存时会按顺序查找: Service Worker --> Memory Cache --> Disk Cache --> Push Cache。
基于 Expires 字段
在以前,我们通常会使用响应头的 Expires 字段去实现强缓存(HTTP/1.0)
Expires 字段的作用:设定一个强缓存时间,在此时间范围内,则从内存(或磁盘)中读取缓存返回。
实现机制:获取本地时间戳,并对先前拿到的资源文件中的 Expires 字段的时间做比较,来判断是否需要对服务器发起请求。
现在 Expires 不再是实现强缓存的首选,原因是 Expires 过度依赖本地时间,如果本地时间与服务器时间不同步,就会出现资源无法被缓存或者资源永远被缓存的情况。
基于 Cache-Control 字段
Cache-Control 响应头字段在 HTTP/1.1 中被增加,解决了 Expires 本地时间和服务器时间不同步的问题,是当下的项目中实现强缓存的最常规方法。
Cache-Control: max-age=10
Cache-Control:max-age=N,N 就是需要缓存的秒数。从服务器第一次返回该资源时开始,往后 N 秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互,不需要比对客户端和服务端的时间,解决了 Expires 所存在的巨大漏洞。
Cache-Control 的属性:
max-age: 客户端资源被缓存多久s-maxage: 代理服务器缓存的时长,需要和public一起用no-cache: 跳过强缓存的校验,强制进行协商缓存no-store: 禁止任何缓存策略public: 资源即可以被浏览器缓存也可以被代理服务器缓存private: 资源只能被浏览器缓存must-revalidate:当缓存过期时,需要去服务端校验缓存的有效性。
no-cache/no-store,public/private 是互斥属性,不能同时出现。
逗号分隔设置多个值:
Cache-control:max-age=10000,s-maxage=200000,public
Pragma
这个是 HTTP/1.0 中禁用网页缓存的字段,其取值为no-cache,和 Cache-Control的 no-cache 效果一样。
协商缓存
基于 Last-Modified 字段
基于 Last-Modified 的协商缓存实现方式: 比较时间戳
第一次请求:
- 在服务器端读出文件修改时间
- 将读出来的修改时间赋给响应头的
Last-Modified字段 - 最后设置
Cache-Control:no-cache
const http = require('http')
const fs = require('fs')
http.createServer((req, res) => {
if(req.url === '/') {
const data = fs.readFileSync('./index.html')
res.end(data)
} else if(req.url === '/bg.png') {
const data = fs.readFileSync('./bg.png')
const { mtime } = fs.statSync('./bg.png')
res.setHeader('Last-Modified', mtime.toUTCString())
res.setHeader('Cache-Control', 'no-cache')
res.end(data)
} else {
res.statusCode = 404
}
}).listen(8080, () => console.log('启动成功: 8080'))
当客户端读取到 Last-Modified 的时候,会在下次的请求头中携带一个字段:If-Modified-Since。
If-Modified-Since: Tue, 18 Jan 2022 01:30:41 GMT
这个 If-Modified-Since 字段是服务端传的 Last-Modified 的值。之后每次对该资源的请求,都会带上 If-Modified-Since 这个字段,而服务端就需要拿到这个时间并再次读取该资源的修改时间,让他们两个做一个比对来决定是读取缓存还是返回新的资源。
if(req.url === '/bg.png') {
const data = fs.readFileSync('./bg.png')
const { mtime } = fs.statSync('./bg.png')
const ifModifiedSince = req.headers['If-Modified-Since']
if(ifModifiedSince === mtime.toUTCString()) {
// 如果一致,则说明文件没有修改过,返回304
res.statusCode = 304
// 因为缓存已经生效了,所以不需要返回资源data
res.end()
return
}
res.setHeader('Last-Modified', mtime.toUTCString())
res.setHeader('Cache-Control', 'no-cache')
res.end(data)
}
存在问题:
- 根据文件修改时间来判断,有可能修改时间变了但文件没修改(比如修改文件名再改回来),但缓存依然失效了。
- 文件修改时间记录的最小单位是秒,如果文件在极短时间内完成修改的时候(比如几百毫秒),文件修改时间不会改变,这样即使文件内容修改了,也不会返回新的文件。
基于 ETag 字段
为了解决上述的问题,从 HTTP/1.1 开始新增了一个头信息: ETag (Entity 实体标签)
ETag 就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。
文件指纹:根据文件内容计算出的唯一哈希值,文件内容一旦改变则指纹改变。
算法:md5、SHA-1、SHA-2、CRC
从校验流程上来说,协商缓存的修改时间比对和文件指纹比对,几乎是一样的。
第一次请求:
- 服务端计算文件指纹
- 将读出来的文件指纹赋给响应头的
Etag字段 - 最后设置
Cache-control:no-cache
后续请求同一个资源:
- 客户端自动从缓存中读取出上一次服务端返回
ETag赋给请求头的if-None-Match字段 - 服务端重新计算文件指纹并和
if-None-Match对比
Etag 缺点:
- 服务端需要更多的计算开销,如果文件尺寸大,数量多,并且计算频繁,将会影响服务器的性能。
ETag有强验证和弱验证:- 强验证(计算成本):生成的哈希码深入到每个字节,哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变,但非常消耗计算量。
- 弱验证(计算误差):提取文件的部分属性来生成哈希值,不必精确到每个字节,整体速度会比强验证快但准确率不高,会降低协商缓存的有效性。
不同于 cache-control 是 expires 的完全替代方案。ETag 并不是 Last-Modified 的完全替代方案,而是 Last-Modified 的补充方案,根据业务场景使用,没有谁更好谁更坏。
禁用缓存
服务器禁用缓存
Cache-Control: max-age=0, must-revalidate
Cache-Control: no-cache
Cache-Control: no-store
浏览器禁用缓存
- 改变
url,加上参数,比如:?_=Date.now() F12打开控制台网络tab,勾选停用缓存- 设置请求
header
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
其他
什么文件适合强缓存?
webpack打包后生成文件名带哈希值或版本号的文件,比如js、css文件
什么文件采用协商缓存?
- 比如
index.html这种需要直接访问的资源 - 页面中动态内容中的图片等文件
有些缓存是从磁盘读取,有些缓存是从内存读取,有什么区别?
- 从内存读取的缓存更快
所有带 304 的资源都是协商缓存,所有标注(从内存/磁盘中读取)的资源都是强缓存。