Node.js HTTP 请求的缓存策略与实现
理解 HTTP 请求缓存
在深入探讨 Node.js 中 HTTP 请求的缓存策略与实现之前,我们先来回顾一下 HTTP 请求缓存的基本概念。HTTP 缓存是一种通过保存资源副本,在后续请求中直接使用副本而无需再次从服务器获取资源的机制。这大大提高了 Web 应用的性能,减少了网络传输和服务器负载。
HTTP 缓存主要分为两种类型:强缓存和协商缓存。
强缓存
强缓存允许浏览器在缓存有效期内直接从本地缓存中加载资源,而不向服务器发送请求。这是通过 Cache-Control
和 Expires
两个 HTTP 头字段来控制的。
- Cache-Control:这是一个相对时间的缓存控制字段,它可以设置多种指令。例如,
Cache-Control: max-age=3600
表示资源在 3600 秒(1 小时)内是有效的,在这个时间内浏览器会直接从缓存中加载资源。常见的指令还有public
(表示资源可以被任何缓存(包括中间代理)缓存)、private
(表示资源只能被用户浏览器缓存)、no-cache
(表示不使用强缓存,需要进行协商缓存验证)、no-store
(表示不缓存任何内容)等。
// 在 Node.js 中设置 Cache-Control 头
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader('Cache-Control','max-age=3600, public');
res.end('This is a cached response');
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
- Expires:这是一个绝对时间的缓存控制字段,它的值是一个 GMT 格式的日期字符串。例如,
Expires: Thu, 18 Dec 2025 12:00:00 GMT
表示资源在这个日期之前是有效的。不过,由于Expires
依赖于客户端和服务器的时间同步,存在一定的局限性,所以现在更推荐使用Cache-Control
。
// 在 Node.js 中设置 Expires 头
const http = require('http');
const server = http.createServer((req, res) => {
const expiresDate = new Date();
expiresDate.setDate(expiresDate.getDate() + 1); // 设置缓存有效期为明天
res.setHeader('Expires', expiresDate.toUTCString());
res.end('This is a cached response');
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
协商缓存
当强缓存失效时,浏览器会发起一个带有验证信息的请求到服务器,服务器根据验证信息来判断资源是否有更新。如果资源没有更新,服务器返回 304 Not Modified 状态码,浏览器则从本地缓存中加载资源;如果资源有更新,服务器返回最新的资源和 200 OK 状态码。协商缓存主要通过 Last-Modified
/If-Modified-Since
和 ETag
/If-None-Match
这两对 HTTP 头字段来实现。
- Last-Modified/
If-Modified-Since
:Last-Modified
头字段表示资源最后一次修改的时间,服务器在响应中返回这个字段。浏览器下次请求时,会在请求头中带上If-Modified-Since
,其值为上次响应中Last-Modified
的值。服务器接收到请求后,比较If-Modified-Since
和资源当前的最后修改时间,如果一致则返回 304 Not Modified,否则返回最新资源。
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer((req, res) => {
const filePath = path.join(__dirname, 'index.html');
const stats = fs.statSync(filePath);
res.setHeader('Last-Modified', stats.mtime.toUTCString());
const ifModifiedSince = req.headers['if-modified-since'];
if (ifModifiedSince && ifModifiedSince === stats.mtime.toUTCString()) {
res.writeHead(304);
res.end();
} else {
const fileContent = fs.readFileSync(filePath);
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(fileContent);
}
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
- ETag/
If-None-Match
:ETag
是资源的唯一标识符,通常是资源内容的哈希值。服务器在响应中返回ETag
,浏览器下次请求时,在请求头中带上If-None-Match
,其值为上次响应中ETag
的值。服务器比较If-None-Match
和当前资源的ETag
,如果一致则返回 304 Not Modified,否则返回最新资源。
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');
const fileContent = fs.readFileSync(filePath);
const etag = crypto.createHash('sha256').update(fileContent).digest('hex');
res.setHeader('ETag', etag);
const ifNoneMatch = req.headers['if-none-match'];
if (ifNoneMatch && ifNoneMatch === etag) {
res.writeHead(304);
res.end();
} else {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(fileContent);
}
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
Node.js 中 HTTP 请求缓存策略的实现
在 Node.js 中实现 HTTP 请求缓存策略,我们可以基于原生的 http
模块或者使用一些流行的框架和库来简化操作。下面我们分别来看一些实现方式。
使用原生 http 模块实现缓存
我们前面已经展示了一些使用原生 http
模块设置缓存相关 HTTP 头字段的示例。为了更完整地实现一个支持缓存的 HTTP 服务器,我们可以将不同的缓存策略整合起来。
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');
const stats = fs.statSync(filePath);
const fileContent = fs.readFileSync(filePath);
const etag = crypto.createHash('sha256').update(fileContent).digest('hex');
// 设置强缓存
res.setHeader('Cache-Control','max-age=3600, public');
res.setHeader('Expires', (new Date(Date.now() + 3600 * 1000)).toUTCString());
// 协商缓存
const ifModifiedSince = req.headers['if-modified-since'];
const ifNoneMatch = req.headers['if-none-match'];
if ((ifModifiedSince && ifModifiedSince === stats.mtime.toUTCString()) ||
(ifNoneMatch && ifNoneMatch === etag)) {
res.writeHead(304);
res.end();
} else {
res.setHeader('Last-Modified', stats.mtime.toUTCString());
res.setHeader('ETag', etag);
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(fileContent);
}
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
在这个示例中,我们同时设置了强缓存和协商缓存。服务器首先设置 Cache-Control
和 Expires
头字段来启用强缓存。然后,对于协商缓存,它会检查 If-Modified-Since
和 If-None-Match
头字段,并根据比较结果返回相应的状态码和内容。
使用 Express 框架实现缓存
Express 是 Node.js 中最流行的 Web 应用框架之一,它提供了更简洁的方式来实现 HTTP 请求缓存。我们可以使用 express
模块结合 serve-static
中间件来设置缓存策略。
首先,安装所需的依赖:
npm install express serve-static
然后,编写 Express 应用:
const express = require('express');
const serveStatic = require('serve-static');
const app = express();
// 设置静态文件目录,并启用缓存
app.use(serveStatic(__dirname, {
maxAge: 3600, // 设置强缓存有效期为 1 小时
setHeaders: (res, path) => {
const stats = fs.statSync(path);
res.set('Last-Modified', stats.mtime.toUTCString());
const etag = crypto.createHash('sha256').update(fs.readFileSync(path)).digest('hex');
res.set('ETag', etag);
}
}));
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
在这个 Express 应用中,我们使用 serve-static
中间件来服务静态文件。通过 maxAge
选项设置了强缓存的有效期为 1 小时。同时,在 setHeaders
回调函数中,我们手动设置了 Last-Modified
和 ETag
头字段,以支持协商缓存。
使用 Koa 框架实现缓存
Koa 是另一个流行的 Node.js Web 框架,它以其简洁的语法和强大的中间件机制而受到青睐。我们可以使用 koa
结合 koa-static
中间件来实现 HTTP 请求缓存。
首先,安装依赖:
npm install koa koa-static
然后,编写 Koa 应用:
const Koa = require('koa');
const serve = require('koa-static');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const app = new Koa();
// 设置静态文件目录,并启用缓存
app.use(serve(__dirname, {
maxage: 3600 * 1000, // 设置强缓存有效期为 1 小时
setHeaders: (res, filePath) => {
const stats = fs.statSync(filePath);
res.set('Last-Modified', stats.mtime.toUTCString());
const etag = crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
res.set('ETag', etag);
}
}));
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
在这个 Koa 应用中,koa-static
中间件用于服务静态文件。maxage
选项设置了强缓存的有效期,setHeaders
回调函数用于设置协商缓存所需的 Last-Modified
和 ETag
头字段。
缓存策略的优化与注意事项
在实现 HTTP 请求缓存策略时,有一些优化点和注意事项需要我们关注。
缓存粒度的控制
缓存粒度指的是我们缓存资源的详细程度。对于一些经常变化的资源,如动态生成的 HTML 页面,我们可能不希望进行长时间的缓存,甚至不缓存。而对于静态资源,如图像、样式表和脚本文件,我们可以设置较长的缓存时间。通过合理控制缓存粒度,可以在提高性能的同时,确保用户获取到最新的内容。
例如,在 Express 应用中,我们可以根据文件的扩展名来设置不同的缓存策略:
const express = require('express');
const serveStatic = require('serve-static');
const app = express();
// 为不同类型的文件设置不同的缓存策略
app.use('/static', serveStatic(__dirname + '/static', {
maxAge: {
'.html': 0, // 不缓存 HTML 文件
'.css': 3600 * 24 * 7 * 1000, // 缓存 CSS 文件一周
'.js': 3600 * 24 * 7 * 1000, // 缓存 JS 文件一周
'.png': 3600 * 24 * 30 * 1000, // 缓存 PNG 图片一个月
'.jpg': 3600 * 24 * 30 * 1000 // 缓存 JPG 图片一个月
}
}));
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
缓存更新的处理
当资源发生变化时,我们需要确保缓存能够及时更新。对于强缓存,一种常见的做法是在资源文件名中添加版本号或者哈希值。例如,将 styles.css
改为 styles_v1.0.css
或者 styles_abc123.css
,这样当资源内容变化时,文件名也会改变,浏览器会认为这是一个新的资源,从而不会使用旧的缓存。
对于协商缓存,我们需要确保 Last-Modified
和 ETag
的值能够准确反映资源的变化。如果资源的更新逻辑比较复杂,可能需要手动更新这些值。
缓存穿透、缓存雪崩和缓存击穿
- 缓存穿透:指查询一个不存在的数据,由于缓存中没有,每次都会去查询数据库,从而给数据库带来压力。解决方法可以是在缓存中设置一个特殊值(如
null
)来表示该数据不存在,这样下次查询相同数据时,直接从缓存中获取null
,而不会查询数据库。 - 缓存雪崩:指大量的缓存数据在同一时间过期,导致大量请求直接落到数据库上。解决方法可以是为不同的缓存设置不同的过期时间,避免集中过期。
- 缓存击穿:指一个高并发访问的热点数据在缓存过期的瞬间,大量请求同时查询数据库,导致数据库压力瞬间增大。解决方法可以是使用互斥锁,在缓存过期时,只有一个请求能够查询数据库并更新缓存,其他请求等待缓存更新后再从缓存中获取数据。
结语
在 Node.js 中实现 HTTP 请求的缓存策略对于提高 Web 应用的性能和降低服务器负载至关重要。通过合理运用强缓存和协商缓存,结合不同的框架和库,我们可以有效地优化应用的缓存机制。同时,注意缓存粒度的控制、缓存更新的处理以及避免缓存穿透、雪崩和击穿等问题,能够让我们的缓存策略更加健壮和高效。希望本文的内容能帮助你在实际项目中更好地实现和优化 HTTP 请求缓存。