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

Node.js 使用缓存中间件提升 API 效率

2022-02-111.8k 阅读

一、Node.js 与 API 开发概述

在当今的 Web 开发领域,Node.js 凭借其异步 I/O 和事件驱动的架构,成为构建高性能 API 的热门选择。Node.js 基于 Chrome V8 引擎,能够高效地处理大量并发请求,使得它在实时应用、微服务架构等场景中表现出色。

当我们构建 API 时,通常需要处理各种数据获取和计算任务。例如,从数据库中查询数据、调用第三方 API 获取信息或者进行复杂的业务逻辑计算。这些操作往往会消耗一定的时间和资源,如果每次请求都重复执行相同的操作,会导致 API 的响应速度变慢,资源利用率降低。

二、缓存的概念与作用

缓存,简单来说,就是将经常访问的数据临时存储在一个快速访问的地方,当下次再次请求相同数据时,直接从缓存中获取,而不需要再次执行原本复杂的数据获取或计算操作。

在 API 开发中,缓存具有以下重要作用:

  1. 提高响应速度:直接从缓存中读取数据要比从数据库或通过网络调用第三方 API 快得多,大大缩短了 API 的响应时间,提升用户体验。
  2. 降低资源消耗:减少对数据库等后端资源的频繁访问,降低数据库负载,提高整个系统的稳定性和可扩展性。
  3. 应对高并发:在高并发场景下,缓存可以有效地减轻后端压力,避免因大量重复请求导致的系统崩溃。

三、Node.js 中的缓存中间件

在 Node.js 生态系统中,有多种缓存中间件可供选择,比如 express - cache - responsenode - cache 等。下面我们以 node - cache 为例,介绍如何在 Node.js 中使用缓存中间件提升 API 效率。

node - cache 是一个简单易用的缓存库,支持多种缓存策略,如内存缓存、文件缓存等。它提供了简洁的 API 来管理缓存数据。

(一)安装 node - cache

首先,我们需要在项目中安装 node - cache。在项目目录下执行以下命令:

npm install node - cache

(二)基本使用示例

以下是一个简单的 Express.js 应用,使用 node - cache 对 API 响应进行缓存:

const express = require('express');
const NodeCache = require('node - cache');

const app = express();
const cache = new NodeCache();

// 模拟一个耗时的 API 调用
const getData = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ data: 'This is some data' });
        }, 2000);
    });
};

app.get('/api/data', async (req, res) => {
    const cachedData = cache.get('apiData');
    if (cachedData) {
        return res.json(cachedData);
    }

    const newData = await getData();
    cache.set('apiData', newData);
    res.json(newData);
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中:

  1. 我们引入了 expressnode - cache 模块。
  2. 创建了一个 NodeCache 实例 cache
  3. 定义了一个模拟耗时操作的 getData 函数,这里使用 setTimeout 模拟了一个 2 秒的延迟。
  4. /api/data 路由中,首先检查缓存中是否存在 apiData。如果存在,直接返回缓存数据;如果不存在,则调用 getData 获取新数据,将新数据存入缓存,并返回给客户端。

四、缓存策略与配置

(一)设置缓存过期时间

node - cache 支持设置缓存数据的过期时间。通过在 set 方法中传入第三个参数来指定过期时间(单位为秒)。

app.get('/api/data', async (req, res) => {
    const cachedData = cache.get('apiData');
    if (cachedData) {
        return res.json(cachedData);
    }

    const newData = await getData();
    // 设置缓存过期时间为 60 秒
    cache.set('apiData', newData, 60);
    res.json(newData);
});

这样,缓存中的 apiData 会在 60 秒后自动过期,下次请求时会重新获取数据并更新缓存。

(二)缓存清除策略

在某些情况下,我们需要主动清除缓存数据。例如,当数据发生变化时,需要确保缓存中的数据也是最新的。node - cache 提供了 del 方法来删除指定的缓存项。

// 假设这是一个更新数据的 API
app.post('/api/updateData', async (req, res) => {
    // 执行数据更新操作
    await updateDataInDatabase(req.body);
    // 清除缓存
    cache.del('apiData');
    res.json({ message: 'Data updated and cache cleared' });
});

在上述代码中,当 /api/updateData API 被调用并成功更新数据库中的数据后,通过 cache.del('apiData') 清除了缓存中的 apiData,确保下次请求 /api/data 时会获取最新的数据。

(三)缓存存储策略

node - cache 默认使用内存存储缓存数据。对于一些需要持久化缓存或者在多进程环境下共享缓存的场景,可以选择使用文件存储或其他分布式缓存方案。例如,使用 node - cache 的文件存储功能:

const cache = new NodeCache({
    stdTTL: 60, // 默认过期时间 60 秒
    checkperiod: 120, // 检查过期数据的周期 120 秒
    useFile: true,
    filePath: './cache.json'
});

在上述配置中,通过设置 useFile: truefilePath 来启用文件存储,并指定缓存数据存储的文件路径为 ./cache.json。这样,缓存数据会在程序运行过程中同步到文件中,即使程序重启,缓存数据依然存在(前提是缓存未过期)。

五、缓存穿透、缓存雪崩与缓存击穿问题及解决方案

(一)缓存穿透

  1. 问题描述:缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,导致请求每次都绕过缓存直接查询数据库。如果有大量这样的请求,会给数据库带来巨大压力,甚至导致数据库崩溃。
  2. 解决方案
    • 布隆过滤器:布隆过滤器是一种空间效率很高的概率型数据结构,可以用来判断一个元素是否在一个集合中。在 API 应用中,我们可以在缓存之前使用布隆过滤器来过滤掉一定不存在的数据。当请求到达时,先通过布隆过滤器判断数据是否可能存在,如果不存在,则直接返回,不再查询数据库。
    • 空值缓存:当查询数据库发现数据不存在时,也将空值存入缓存,并设置一个较短的过期时间。这样下次相同请求就会直接从缓存中获取空值,而不会查询数据库。
app.get('/api/data/:id', async (req, res) => {
    const { id } = req.params;
    const cachedData = cache.get(`data_${id}`);
    if (cachedData) {
        // 如果缓存为空值,直接返回
        if (cachedData === null) {
            return res.json({ message: 'Data not found' });
        }
        return res.json(cachedData);
    }

    const data = await getDataFromDatabase(id);
    if (data) {
        cache.set(`data_${id}`, data);
        res.json(data);
    } else {
        // 缓存空值,过期时间设为 60 秒
        cache.set(`data_${id}`, null, 60);
        res.json({ message: 'Data not found' });
    }
});

(二)缓存雪崩

  1. 问题描述:缓存雪崩是指在同一时刻大量的缓存数据同时过期,导致大量请求直接访问数据库,造成数据库压力骤增,甚至可能使数据库崩溃。
  2. 解决方案
    • 随机过期时间:在设置缓存过期时间时,不使用固定的过期时间,而是设置一个随机的过期时间范围。例如,原本过期时间为 60 秒,可以设置为 50 - 70 秒之间的随机值。这样可以避免大量缓存同时过期。
app.get('/api/data', async (req, res) => {
    const cachedData = cache.get('apiData');
    if (cachedData) {
        return res.json(cachedData);
    }

    const newData = await getData();
    // 设置随机过期时间 50 - 70 秒
    const randomTTL = Math.floor(Math.random() * 21) + 50;
    cache.set('apiData', newData, randomTTL);
    res.json(newData);
});
- **二级缓存**:采用二级缓存方案,主缓存失效时,从二级缓存获取数据。二级缓存可以使用不同的过期策略或者存储介质,以降低同时失效的风险。

(三)缓存击穿

  1. 问题描述:缓存击穿是指一个热点数据在缓存过期的瞬间,大量请求同时访问,导致这些请求全部直接访问数据库,造成数据库压力过大。
  2. 解决方案
    • 互斥锁:在缓存过期时,使用互斥锁(如 redisSETNX 命令,在 node - cache 场景下可通过自定义逻辑实现类似效果)来保证只有一个请求能够查询数据库并更新缓存,其他请求等待。当第一个请求更新完缓存后,其他请求就可以从缓存中获取数据。
const mutex = false;
app.get('/api/hotData', async (req, res) => {
    const cachedData = cache.get('hotData');
    if (cachedData) {
        return res.json(cachedData);
    }

    if (!mutex) {
        // 设置互斥锁
        mutex = true;
        const newData = await getData();
        cache.set('hotData', newData);
        // 释放互斥锁
        mutex = false;
        res.json(newData);
    } else {
        // 等待一段时间后重试
        setTimeout(() => {
            res.redirect('/api/hotData');
        }, 100);
    }
});

六、结合实际业务场景优化缓存策略

(一)电商商品 API 缓存

在电商应用中,商品详情 API 是一个典型的需要缓存优化的场景。商品数据相对稳定,更新频率较低。

  1. 缓存粒度:可以根据商品 ID 进行缓存,每个商品的详情作为一个独立的缓存项。这样在某个商品数据更新时,只需要清除该商品对应的缓存,而不影响其他商品的缓存。
  2. 缓存更新策略:当商品数据发生变化(如价格调整、库存变更等)时,及时清除对应的缓存。可以通过监听数据库的变更事件或者在商品更新 API 中手动清除缓存。
// 商品详情 API
app.get('/api/products/:productId', async (req, res) => {
    const { productId } = req.params;
    const cachedProduct = cache.get(`product_${productId}`);
    if (cachedProduct) {
        return res.json(cachedProduct);
    }

    const product = await getProductFromDatabase(productId);
    if (product) {
        cache.set(`product_${productId}`, product);
        res.json(product);
    } else {
        res.json({ message: 'Product not found' });
    }
});

// 商品更新 API
app.put('/api/products/:productId', async (req, res) => {
    const { productId } = req.params;
    // 执行商品更新操作
    await updateProductInDatabase(productId, req.body);
    // 清除缓存
    cache.del(`product_${productId}`);
    res.json({ message: 'Product updated and cache cleared' });
});

(二)社交平台动态 API 缓存

社交平台的用户动态 API 具有数据实时性要求较高的特点。

  1. 缓存策略:可以采用时间窗口缓存策略,例如缓存最近 5 分钟内的用户动态。对于超出时间窗口的动态,重新从数据库获取并更新缓存。
  2. 实时性处理:为了保证一定的实时性,当有新的动态发布时,除了更新数据库,还需要及时更新缓存。可以通过消息队列(如 RabbitMQ、Kafka 等)来异步处理缓存更新,减少对 API 响应时间的影响。
// 用户动态 API
app.get('/api/user/:userId/feed', async (req, res) => {
    const { userId } = req.params;
    const cachedFeed = cache.get(`user_feed_${userId}`);
    if (cachedFeed) {
        return res.json(cachedFeed);
    }

    const feed = await getFeedFromDatabase(userId);
    if (feed) {
        // 缓存 5 分钟
        cache.set(`user_feed_${userId}`, feed, 300);
        res.json(feed);
    } else {
        res.json({ message: 'No feed available' });
    }
});

// 新动态发布 API
app.post('/api/user/:userId/post', async (req, res) => {
    const { userId } = req.params;
    // 发布新动态到数据库
    await postNewFeedToDatabase(userId, req.body);
    // 通过消息队列异步更新缓存
    messageQueue.send({
        type: 'updateUserFeed',
        userId,
        newFeed: req.body
    });
    res.json({ message: 'Feed posted successfully' });
});

七、性能测试与监控

(一)性能测试工具

为了验证缓存中间件对 API 性能的提升效果,我们可以使用工具如 Apache JMeterArtillery 等进行性能测试。

  1. 使用 Artillery 进行测试
    • 安装 Artillery:npm install -g artillery
    • 创建测试脚本 test.yml
config:
  target: 'http://localhost:3000'
  phases:
    - duration: 60
      arrivalRate: 100
scenarios:
  - flow:
      - get:
          url: '/api/data'
- 执行测试:`artillery run test.yml`

通过 Artillery 的测试结果,我们可以获取 API 的响应时间、吞吐量等性能指标,对比使用缓存前后的性能变化。

(二)监控缓存状态

在实际运行过程中,监控缓存的状态也是非常重要的。node - cache 提供了一些方法来获取缓存的统计信息,如缓存项数量、命中次数、未命中次数等。

// 获取缓存统计信息
const stats = cache.getStats();
console.log(`Cache items: ${stats.items}`);
console.log(`Hit count: ${stats.hits}`);
console.log(`Miss count: ${stats.misses}`);

通过定期记录这些统计信息,我们可以了解缓存的使用情况,及时发现缓存命中率低等问题,并针对性地调整缓存策略。

八、与其他技术结合优化 API 效率

(一)与 CDN 结合

CDN(内容分发网络)可以将静态资源(如图片、脚本、样式文件等)缓存到离用户更近的节点,加快用户访问速度。在 API 开发中,如果 API 响应包含一些静态资源链接,结合 CDN 可以进一步提升整体的用户体验。例如,将图片存储在 CDN 上,API 只返回图片的 CDN 链接。

// 假设这是一个返回商品图片链接的 API
app.get('/api/products/:productId/image', async (req, res) => {
    const { productId } = req.params;
    const product = await getProductFromDatabase(productId);
    if (product) {
        const cdnUrl = `https://cdn.example.com/products/${product.image}`;
        res.json({ imageUrl: cdnUrl });
    } else {
        res.json({ message: 'Product not found' });
    }
});

(二)与 GraphQL 结合

GraphQL 是一种用于 API 的查询语言,它允许客户端精确地请求所需的数据,避免了传统 REST API 中可能出现的过度获取或获取不足的问题。与缓存中间件结合使用时,可以根据 GraphQL 查询的具体内容进行缓存。例如,不同的 GraphQL 查询可能对应不同的缓存键,确保缓存的准确性和高效性。

// 使用 Apollo Server 搭建 GraphQL 服务
const { ApolloServer, gql } = require('apollo - server - express');
const express = require('express');
const NodeCache = require('node - cache');

const app = express();
const cache = new NodeCache();

// 定义 GraphQL 类型和查询
const typeDefs = gql`
    type Query {
        user(id: ID!): User
    }
    type User {
        id: ID
        name: String
    }
`;

const resolvers = {
    Query: {
        user: async (_, { id }) => {
            const cacheKey = `graphql_user_${id}`;
            const cachedUser = cache.get(cacheKey);
            if (cachedUser) {
                return cachedUser;
            }

            const user = await getUserFromDatabase(id);
            if (user) {
                cache.set(cacheKey, user);
                return user;
            }
            return null;
        }
    }
};

const server = new ApolloServer({ typeDefs, resolvers });
server.applyMiddleware({ app });

const port = 4000;
app.listen(port, () => {
    console.log(`GraphQL server running on port ${port}`);
});

通过以上多种方式的结合,我们可以全方位地提升 Node.js API 的效率,为用户提供更加流畅、快速的服务体验。在实际应用中,需要根据具体的业务需求和场景,灵活选择和组合这些技术,以达到最佳的性能优化效果。同时,持续的性能测试和监控也是确保系统稳定高效运行的关键环节。