MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js HTTP 请求的缓存策略与实现

2023-12-285.4k 阅读

理解 HTTP 请求缓存

在深入探讨 Node.js 中 HTTP 请求的缓存策略与实现之前,我们先来回顾一下 HTTP 请求缓存的基本概念。HTTP 缓存是一种通过保存资源副本,在后续请求中直接使用副本而无需再次从服务器获取资源的机制。这大大提高了 Web 应用的性能,减少了网络传输和服务器负载。

HTTP 缓存主要分为两种类型:强缓存和协商缓存。

强缓存

强缓存允许浏览器在缓存有效期内直接从本地缓存中加载资源,而不向服务器发送请求。这是通过 Cache-ControlExpires 两个 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-SinceETag/If-None-Match 这两对 HTTP 头字段来实现。

  • Last-Modified/If-Modified-SinceLast-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-MatchETag 是资源的唯一标识符,通常是资源内容的哈希值。服务器在响应中返回 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-ControlExpires 头字段来启用强缓存。然后,对于协商缓存,它会检查 If-Modified-SinceIf-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-ModifiedETag 头字段,以支持协商缓存。

使用 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-ModifiedETag 头字段。

缓存策略的优化与注意事项

在实现 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-ModifiedETag 的值能够准确反映资源的变化。如果资源的更新逻辑比较复杂,可能需要手动更新这些值。

缓存穿透、缓存雪崩和缓存击穿

  • 缓存穿透:指查询一个不存在的数据,由于缓存中没有,每次都会去查询数据库,从而给数据库带来压力。解决方法可以是在缓存中设置一个特殊值(如 null)来表示该数据不存在,这样下次查询相同数据时,直接从缓存中获取 null,而不会查询数据库。
  • 缓存雪崩:指大量的缓存数据在同一时间过期,导致大量请求直接落到数据库上。解决方法可以是为不同的缓存设置不同的过期时间,避免集中过期。
  • 缓存击穿:指一个高并发访问的热点数据在缓存过期的瞬间,大量请求同时查询数据库,导致数据库压力瞬间增大。解决方法可以是使用互斥锁,在缓存过期时,只有一个请求能够查询数据库并更新缓存,其他请求等待缓存更新后再从缓存中获取数据。

结语

在 Node.js 中实现 HTTP 请求的缓存策略对于提高 Web 应用的性能和降低服务器负载至关重要。通过合理运用强缓存和协商缓存,结合不同的框架和库,我们可以有效地优化应用的缓存机制。同时,注意缓存粒度的控制、缓存更新的处理以及避免缓存穿透、雪崩和击穿等问题,能够让我们的缓存策略更加健壮和高效。希望本文的内容能帮助你在实际项目中更好地实现和优化 HTTP 请求缓存。