缓存的定义
缓存是计算机中一种常见的技术,用于临时存储数据,以便快速访问。它在多种计算机系统和应用程序中都有广泛应用,以提高性能和效率。
缓存的基本原理是利用临时存储器(通常是高速缓存)存储最近或频繁使用的数据,以便将来能够更快地访问这些数据,而无需再次从原始数据源获取。这有助于减少对慢速数据源(如硬盘或网络)的访问次数,从而提高系统的响应速度和整体性能。
缓存可以分为多个层次,通常包括:
CPU缓存:位于处理器内部的高速缓存,用于存储最近访问的指令和数据,以提高CPU的执行效率。
内存缓存:位于主存(RAM)和CPU之间的高速缓存,用于存储主存中频繁访问的数据块,以减少对主存的访问延迟。
硬盘缓存:位于硬盘驱动器内部或操作系统内存中的缓存,用于存储磁盘上的数据块,以减少对物理硬盘的读写次数。
网络缓存:位于网络服务器或客户端内部的缓存,用于存储最近访问或请求的网络资源(如网页、图片等),以减少对远程服务器的访问。
缓存的实现可以通过硬件、操作系统或应用程序来完成。在应用程序中,开发人员通常会使用缓存来存储经常使用的数据,以减少对数据库或其他数据源的访问,从而提高应用程序的性能和响应速度。缓存还可以根据数据的特性和访问模式采用不同的策略,如LRU(最近最少使用)、LFU(最不常用)、FIFO(先进先出)等。
总的来说,缓存是一种重要的性能优化技术,可以在各种计算机系统和应用程序中发挥作用,提高数据访问的效率和速度。
什么是HTTP缓存?
本文着重讨论http缓存,它属于网络缓存的一种。
HTTP缓存是指在HTTP协议中使用的一种缓存机制,用于在客户端和服务器之间存储和管理Web资源(如网页、图像、脚本文件等)的副本。HTTP缓存旨在减少网络流量、提高网站性能和加快页面加载速度。 HTTP缓存通常基于以下两种核心机制:
- 直接缓存(Freshness-based Caching):在直接缓存中,客户端根据缓存策略直接使用本地缓存副本,而不需要向服务器发送请求进行验证。这种缓存机制基于资源的过期时间或验证标识来确定是否可以使用缓存。常见的直接缓存策略包括:
- Expires:服务器在响应头中返回资源的过期时间,客户端在请求时检查该时间,如果未过期,则使用缓存副本,否则向服务器请求最新资源。
- Cache-Control:通过Cache-Control头部字段,服务器可以指定资源的缓存策略,包括max-age(最大缓存时间)、no-cache(不缓存)等指令。
- 协商缓存(Validation-based Caching):在协商缓存中,客户端发送一个条件请求到服务器,检查本地缓存的资源是否仍然有效。服务器根据条件请求中的信息来判断资源是否已被修改,如果未被修改,则返回状态码304(未修改),告知客户端可以使用缓存副本。常见的协商缓存机制包括:
- Last-Modified / If-Modified-Since:服务器在响应头中返回资源的最后修改时间(Last-Modified),客户端下次请求时在请求头中包含If-Modified-Since字段,如果资源未被修改,则服务器返回304响应。
- ETag / If-None-Match:服务器在响应头中返回资源的标识符(ETag),客户端下次请求时在请求头中包含If-None-Match字段,如果资源未被修改,则服务器返回304响应。
HTTP缓存可以在客户端(如Web浏览器)和服务器(如Web服务器)两端进行配置和管理。开发者可以通过设置HTTP响应头来控制缓存策略,包括缓存有效期、验证标识、缓存指令等,以优化Web资源的传输和利用。正确配置HTTP缓存可以显著提高网站性能和用户体验,减少网络流量和服务器负载。
通常在客户端的http缓存简易原理如下所示:
HTTP缓存怎么实现?
思考以下问题?
- 有哪些资源是需要缓存的?
- 缓存的时间到底是多久呢?
- 如果缓存的资源有变化该怎么处理?
注意:通常情况下,GET 请求是可缓存的,因为它通常用于获取资源,而且不应该具有副作用(不会改变服务器状态)。因此,GET 请求允许被缓存,这样下次再次请求相同的资源时,可以直接从缓存中获取,而无需再次请求服务器。
而 POST、PUT、DELETE 等请求方法通常被认为是不可缓存的,因为它们可能对服务器状态产生影响,或者包含了请求体(request body),使得缓存的复用变得不可行或不安全。然而,即使是这些请求方法,也可以通过一些特殊的情况下的缓存策略来进行缓存,例如如果服务器返回了合适的缓存指令(如 Cache-Control 头部),或者利用一些中间缓存代理的特殊处理等。
总的来说,虽然 GET 请求是最常见的可缓存请求,但其他请求方法在特定条件下也可以进行缓存,这取决于请求方法本身的语义以及服务器返回的响应头信息中指示的缓存指令。
与 HTTP 缓存相关的响应头主要包括:
Cache-Control:指定缓存策略,包括
public
、private
、no-cache
、no-store
、max-age
等指令。Expires:指定响应的过期时间,是一个 HTTP-date 格式的时间戳。在指定的过期时间之后,客户端必须向服务器发起新的请求获取资源。
Last-Modified:指定响应资源的最后修改时间,用于协商缓存。客户端在下次请求时,可以发送
If-Modified-Since
头部字段,服务器根据该值判断资源是否已被修改,如果未被修改则返回 304 Not Modified。ETag:指定响应资源的标识符,用于协商缓存。客户端在下次请求时,可以发送
If-None-Match
头部字段,服务器根据该值判断资源是否已被修改,如果未被修改则返回 304 Not Modified。这些响应头部字段通常由服务器在响应中返回,用于指示客户端和代理服务器如何缓存响应数据,以提高性能和减少网络流量。通过合理配置这些响应头部字段,可以实现灵活高效的 HTTP 缓存策略。
其中Cache-Control
有以下相关的指令:
- public:表明响应可以被任意缓存,包括客户端和代理服务器。
- private:表明响应只能被客户端缓存,不允许被代理服务器缓存。
- no-cache:表明缓存不能直接使用已缓存的响应,而必须先向源服务器验证响应的有效性。
- no-store:表明响应或请求中的任何敏感信息都不应该被存储在缓存中,即禁止缓存。
- max-age=seconds :指定响应的最大有效时间,单位为秒。
- s-maxage=seconds :类似于
max-age
,但只适用于共享缓存,如代理服务器。- must-revalidate:表明在过期之后,缓存必须向源服务器进行验证,以确保响应仍然有效。
- proxy-revalidate:类似于
must-revalidate
,但只适用于代理服务器缓存。- max-stale[=seconds] :允许缓存服务器在资源过期后继续提供过期的响应,
<seconds>
参数可指定最大过期时间。- min-fresh=seconds :要求缓存服务器提供一个在指定时间内仍然有效的响应,如果无法满足,缓存服务器必须重新验证资源。
- only-if-cached:只有在缓存服务器上有可用的响应时,才会被返回;如果没有缓存响应可用,不会向源服务器发送请求。
- no-transform:禁止代理服务器对响应进行转换,保持原始内容。
这些指令可以组合使用,以定义复杂的缓存策略。开发人员可以根据应用程序的需求和特定场景,合理地利用
Cache-Control
字段来优化缓存行为,提高 Web 应用程序的性能和用户体验。
上面那么多的概念看上去可能会比较复杂,不过其实我们只需要关注其中几个重要的点就可以了。
强缓存(直接缓存)
我们来仔细的分析下,从服务器返回的响应头来看,响应时间为
Date: Mon, 29 Jan 2024 06:48:40 GMT(格林威治时间,以下同理
),而且这个资源需要被缓存的:
- 最大的有效时间为
Cache-Control: max-age=2592000秒(30天)
- 响应的过期时间为
Expiires: Sun, 25 Feb 2024 06:42:41 GMT
- 最后的修改时间为
Last-Modified: Mon, 29 Nov 2021 08:08:24 GMT
- 响应资源标识符为
Etag: 61a48a78-11d0(资源的hash)
当客户端收到这个响应之后,如果客户端支持缓存就会按照以上指令把资源加入缓存,为下一次相同的请求做准备。
注意:对于chrome而言,我们可以简单的把缓存的数据格式看成类似于以请求方法+请求路径为key,以资源内容为value的数据结构。
当客户端再次向服务器发出请求时,就会在请求发出之前先从缓存中查找,我们可以简单的理解分为两步:
- 缓存中有没有匹配的
请求方法+请求路径
(缓存中的key
)?- 缓存中的资源是否还在
有效期
?(根据Date+max-age
或者Expires
判断)
如果能够命中缓存并且缓存在有效期内,则客户端就会直接使用缓存的资源,不会再向服务器发出请求,这样可以极大的减少服务器的压力。
注意:Expires
是http1.0的规范,max-age
是http1.1的规范。有关于具体的资源缓存时间的计算规则比较复杂,一般我们只需要大概的了解下就行了,感兴趣的同学可以自行去了解下。
协商缓存
那如果客户端命中缓存后但缓存失效了呢?这个时候客户端会向服务器发送一个加入了缓存信息的请求,一般是通过指定请求头中的以下字段向服务器询问该资源缓存是否已经改变了。
If-Modified-Since: Last-Modified
If-None-Match: Etag
当服务器收到客户端发出的缓存请求时,会通过请求头中的字段来对比该资源缓存是否已经改变了。分为两种情况:
- 资源已经发生改变,直接给客户端响应一个
200
的正常请求,并在响应体中带上新的资源内容,客户端会直接使用新的资源内容,并且会按照新的响应头重新设置缓存,就和一个新的请求一样- 资源没有发生改变,会给客户端响应一个
304 Not Modified
的请求,这种请求没有响应体,只会在响应头中重新设置缓存指令,这样客户端就可以继续使用缓存资源了,并且按照新的响应头更新缓存设置
如果缓存资源资源已经失效但实际并没有改变,客户端可以再次复用缓存资源,这样虽然需要额外发出请求,但是该请求没有响应体,同样可以减少服务器的压力。
注意:If-Modified-Since
是http1.0的规范,If-None-Match
是http1.1的规范。一般来说对于服务器而言,后者的优先级高于前者
启发式缓存
那如果一个请求的响应头中没有Cache-Control
字段呢?客户端是不是就不缓存了?答案其实是否定的。
我们来请求一个资源,注意它的响应头中是没有Cache-Control
的。
再次发起请求,你会发现它仍然走了缓存,这里其实就触发了启发式缓存
。
所以当一个请求没有Cache-Control
字段但是符合某些条件的时候,比如有Last-Modified
字段,那么客户端就会推算这个资源在大概什么时间内并不会发生改变,所以就会把它缓存下来,具体缓存多久要看客户端的具体实现,一般是上次变化时间的10%
,比如这里大概就是5天*10%=半天。
注意:在任何情况下我们都需要明确指定Cache-Control
字段,否则会导致该请求缓存的不确定性,比如资源没做缓存导致频繁请求,或者资源命中了启发式缓存导致导致没有重新请求等各种问题。
实战一下
我们以node
做为服务器以chrome
做为客户端,简单的写一个示例验证一下上面所说的。
<!-- 客户端代码 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>http缓存协议</h1>
<button onclick="fetch('http://localhost:3000/')">请求资源</button>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
</body>
</html>
// 服务端代码
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const server = http.createServer((req, res) => {
const filePath = path.join(__dirname, 'index.html');
// 读取HTML文件内容
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(500);
res.end('Error loading index.html');
return;
}
// 设置响应头
const maxAge = 10;
res.setHeader('Content-Type', 'text/html');
res.setHeader('Cache-Control', `max-age=${maxAge}`);
res.setHeader('Expires', new Date(Date.now() + maxAge).toUTCString());
const lastModified = fs.statSync(filePath).mtime.toUTCString();
const hash = crypto.createHash('md5').update(data).digest('hex');
res.setHeader('Content-Type', 'text/html');
res.setHeader('Last-Modified', lastModified);
res.setHeader('ETag', `"${hash}"`);
// 检查是否需要返回304 Not Modified
if (req.headers['if-modified-since'] === lastModified || req.headers['if-none-match'] === `"${hash}"`) {
res.writeHead(304);
res.end();
return;
}
// 发送HTML内容
res.writeHead(200);
res.end(data);
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
简单的解释下以上代码,启动一个node
服务器提供index.html
文件的访问,设置资源缓存的有效期并在缓存失效时模拟304
响应。运行以上代码,直接访问http://localhost:3000/
即可:
第一次访问
此时因为是第一次请求,所以这就是一个正常的请求,观察响应头会发现资源的缓存时间max-age
为10s
,此时浏览器会按照缓存指令把资源加入缓存。
在缓存有效期再次访问
此时因为发出了同一个请求,经过浏览器判断后,命中缓存且缓存有效,所以浏览器会直接使用缓存,观察该请求的时间和大小会发现相比之下大大减少了。(强缓存
)
在缓存失效后再次访问
此时虽然命中缓存但缓存已经过期了,所以浏览器发送一个缓存请求,观察该请求头,会向服务器发送if-modified-since
或if-none-match
字段。经过服务器验证,该资源虽然缓存已经过期了但资源并没有发生改变,所以会响应一个304的请求,浏览器会再次使用缓存并更新缓存设置。同理,该请求的时间和大小相比第一次请求也会大大的减少了。(协商缓存
)
后续的请求就会一直循环以上过程了。。。
注意:大家可以看一下自己开发过的应用中相关请求,通过观察请求头和响应头分析该请求的行为是否符合预期。
总结
让我们用两张图示意一下上面所学的内容:
服务端
客户端
相关知识
一些细节
http
的缓存策略会因为客户端
或服务器
的不同有所差异- 缓存的匹配机制可能会和一些请求头信息相关,比如
cookie
HTTPS
HTTPS 与 HTTP 在缓存方面的基本原则是相同的,因为 HTTPS 实际上是在传输层上对 HTTP 进行了加密,而并没有改变 HTTP 协议本身。因此,HTTPS 并不会直接影响缓存协议的行为,例如使用 Cache-Control、Expires、Last-Modified、ETag 等头部字段进行缓存控制和验证的机制仍然适用于 HTTPS。
但是,HTTPS 与 HTTP 在缓存方面有一些细微的区别和影响:
- 中间缓存:HTTPS 通常会对内容进行加密传输,因此中间缓存(如代理服务器)可能无法读取加密的内容,从而无法对加密内容进行缓存。这可能会导致 HTTPS 的缓存效果不如 HTTP,尤其是在中间缓存无法缓存响应的情况下。
- 服务器推送:在 HTTP/2 和 HTTP/3 中,服务器推送(Server Push)是一种优化技术,可以让服务器在客户端请求之前主动向客户端推送资源。由于 HTTPS 的安全机制,服务器推送的资源也需要通过加密传输,因此在 HTTPS 连接上使用服务器推送时可能会受到一些限制。
- 证书校验:HTTPS 通信需要双方进行证书验证,这会增加连接建立的时间,可能会对缓存命中率产生影响。尤其是在 TLS 握手阶段需要进行证书验证和密钥交换的过程中,可能会增加一些延迟。
综上所述,虽然 HTTPS 并不会直接改变缓存协议的机制,但它会对缓存的行为产生一些影响,特别是在中间缓存无法读取加密内容、服务器推送资源需要加密传输以及证书校验等方面。因此,在设计和优化基于 HTTPS 的应用时,需要考虑到这些影响因素,以最大程度地发挥缓存的效果。
HTTP2/HTTP3
HTTP/2 和 HTTP/3 在缓存方面都保留了 HTTP/1.1 的基本机制,例如使用 Cache-Control 和 Expires 头部字段控制缓存行为,以及使用 Last-Modified 和 ETag 头部字段进行缓存验证。然而,它们在传输层面的改动可能会影响到缓存的一些行为。
HTTP/2:
HTTP/2 通过多路复用、头部压缩和服务器推送等技术优化了性能,但在缓存方面并没有引入显著的变化。
由于 HTTP/2 使用了多路复用技术,在同一个连接上可以同时传输多个请求和响应,这可能会影响代理服务器或中间缓存的缓存行为,但并不会改变缓存协议本身。
HTTP/3:
- HTTP/3 基于 QUIC 协议,在传输层面对网络连接进行了重大改进,例如提供更快的连接建立和传输速度、减少连接时延等。
- 由于 HTTP/3 使用了 QUIC 协议取代了 TCP,在传输层面上有了较大的改动,可能会对缓存行为产生影响。具体来说,HTTP/3 使用了自定义的 QUIC 流(Stream)来传输数据,这与传统的 TCP 连接有所不同,因此可能需要针对 QUIC 协议进行定制化的缓存策略。
- 此外,HTTP/3 的 0-RTT(Zero Round Trip Time)特性允许在第一次连接时发送数据,这可能会影响缓存的命中率和缓存策略。
总的来说,HTTP/2 和 HTTP/3 在缓存方面并没有引入大的变化,但是由于它们在传输层面上的改动,可能会对一些缓存行为产生影响,特别是在使用 HTTP/3 时可能需要针对 QUIC 协议进行定制化的缓存策略。