NodeJs项目顶不住高并发,来结合 RabbitMQ 消息队列试试吧😏😏😏

2,304 阅读6分钟

假设我们正在开发一个博客的系统,那必然少不了点赞的需求了,但是有一个问题,当多个用户几乎同时点赞同一内容时,如果没有合理的处理机制,可能会出现以下问题:

  1. 数据库的写入冲突:多个用户同时对同一内容进行点赞,可能会导致数据库的竞争条件。

  2. 点赞数不一致:在高并发情况下,更新点赞数可能发生竞争,导致数据库中存储的点赞数与实际点赞数不一致。

  3. 性能瓶颈:数据库频繁更新可能会导致瓶颈,特别是当点赞量特别大的时候。

我们可以通过 RabbitMQ 解耦点赞操作,并异步处理高并发请求。具体来说,当用户进行点赞时,系统将点赞请求发送到 RabbitMQ 队列,由后台消费者异步处理点赞数的更新。这种方式能够有效避免主服务直接处理大量并发请求,减轻数据库的压力,从而实现削峰并提升系统的性能和稳定性。

如何在 NestJs 项目中使用

首先我们需要私有 Docker 创建一个 RabbitMQ 的实例,编写如下配置文件:

version: "3.9"

services:
  rabbitmq:
    image: rabbitmq:3-management
    container_name: rabbitmq
    ports:
      - "5672:5672" # RabbitMQ 主连接端口
      - "15672:15672" # RabbitMQ 管理控制台端口
    environment:
      RABBITMQ_DEFAULT_USER: user
      RABBITMQ_DEFAULT_PASS: password
    networks:
      - rabbitmq-network

networks:
  rabbitmq-network:
    driver: bridge

在上面的配置中我们通过 Docker compose 来创建了一个实例,我们执行如下命令便可以启动该服务了:

docker compose up -d

这下我们的消息队列就搭建起来了,访问 http://localhost:15672/#/ 便可以查阅相关页面:

20241113153442

项目集成

@golevelup/nestjs-rabbitmq 是一个专为 NestJS 框架设计的库,用于集成 RabbitMQ 消息队列。这个库提供了一个非常简单、易于配置和使用的方式来与 RabbitMQ 进行通信,帮助你在 NestJS 中处理消息传递、生产者和消费者模式。

首先,安装 @golevelup/nestjs-rabbitmq 库:

pnpm add @golevelup/nestjs-rabbitmq

接下来我们需要配置 RabbitMQ 的 Model,如下代码所示:

import { RabbitMQModule } from "@golevelup/nestjs-rabbitmq";
import { Module } from "@nestjs/common";

@Module({
  imports: [
    RabbitMQModule.forRoot(RabbitMQModule, {
      uri: "amqp://user:password@localhost:5672",
      exchanges: [
        {
          name: "like_exchange",
          type: "topic",
        },
      ],
    }),
  ],
  exports: [RabbitMQModule],
})
export class RabbitMQConfigModule {}

这段代码的目的是:

  1. 配置和初始化 RabbitMQ 连接,包括 RabbitMQ 的 URI 和交换机的设置。

  2. 使用 RabbitMQModule 来封装 RabbitMQ 的相关配置,使得在 NestJS 中更方便地使用 RabbitMQ 进行消息传递。

  3. 将 RabbitMQModule 导出,使得其他模块可以方便地注入和使用 RabbitMQ。

这样,你就能够在整个 NestJS 项目中统一配置 RabbitMQ 连接,并让其他模块在需要的时候直接使用。

编写完成之后,你可以全局导入,你也可以在有需要的模块中引入:

import { Module } from "@nestjs/common";
import { LikeProducerService } from "./like.service";
import { LikeController } from "./like.controller";
import { LikeConsumerService } from "./consumer.service";
import { RabbitMQConfigModule } from "./rabbitmq.module";

@Module({
  controllers: [LikeController],
  providers: [LikeProducerService, LikeConsumerService],
  imports: [RabbitMQConfigModule],
})
export class LikeModule {}

接下来要编写我们的的 RabbitMQ 消费者 服务了,如下代码所示:

import { Controller, Logger } from "@nestjs/common";
import { RabbitSubscribe } from "@golevelup/nestjs-rabbitmq";
import { Subject } from "rxjs";
import { bufferTime, filter } from "rxjs/operators";

@Controller()
export class LikeConsumerService {
  private readonly logger = new Logger(LikeConsumerService.name);
  private likeCounts: Record<string, number> = {}; // 用于存储每个帖子的点赞数
  private likeSubject = new Subject<{ postId: string }>(); // RxJS 的 Subject,用于触发批量更新

  constructor() {
    // 使用 RxJS 的 bufferTime 操作符来实现每 5 秒批量更新
    this.likeSubject
      .pipe(
        bufferTime(5000), // 每 5 秒收集一次事件
        filter((likes) => likes.length > 0) // 过滤空的批次
      )
      .subscribe(() => this.triggerBulkUpdate());
  }

  @RabbitSubscribe({
    exchange: "like_exchange",
    routingKey: "like",
    queue: "like_queue",
  })
  async handleLikeMessage(message: { userId: string; postId: string }) {
    const { userId, postId } = message;
    this.logger.log(`收到来自用户 ${userId} 的点赞消息,帖子 ID 为 ${postId}`);

    // 增加计数并推送事件到 Subject
    this.likeCounts[postId] = (this.likeCounts[postId] || 0) + 1;
    this.likeSubject.next({ postId });
  }

  // 触发批量更新
  private async triggerBulkUpdate() {
    const likeCountsCopy = { ...this.likeCounts }; // 拷贝数据
    console.log(likeCountsCopy);

    this.likeCounts = {}; // 清空待更新的数据

    this.logger.log(
      `开始进行批量更新操作,点赞数据:${JSON.stringify(likeCountsCopy)}`
    );

    try {
      const updatePromises = Object.entries(likeCountsCopy).map(
        async ([postId, count]) => this.updatePostLikesInDB(postId, count)
      );

      await Promise.all(updatePromises);
      this.logger.log("批量更新操作成功完成");
    } catch (error) {
      this.logger.error("批量更新点赞数失败:", error);
      // 恢复未成功的更新
      Object.entries(likeCountsCopy).forEach(([postId, count]) => {
        this.likeCounts[postId] = (this.likeCounts[postId] || 0) + count;
      });
    }
  }

  // 更新单个帖子点赞数
  private async updatePostLikesInDB(postId: string, count: number) {
    try {
      console.log(`正在更新数据库,帖子 ID: ${postId},点赞数:${count}`);
      // 在这里实现更新数据库的逻辑,例如:
      // await this.postRepository.increment({ id: postId }, 'likeCount', count);
    } catch (error) {
      this.logger.error(`更新帖子 ${postId} 点赞数失败:`, error);
      throw error;
    }
  }
}

这段代码实现了以下功能:

  1. 消费消息:通过 @RabbitSubscribe 装饰器监听 RabbitMQ 消息队列,接收用户的点赞信息(userId 和 postId)。

  2. 批量处理:使用 RxJS 的 bufferTime 操作符将收到的点赞信息按 5 秒的间隔进行批量收集,然后触发批量更新操作。这样可以减少对数据库的压力,避免频繁更新。

  3. 批量更新:在 5 秒内收集到的点赞数会通过 triggerBulkUpdate 方法进行批量更新,更新完成后清空缓存的点赞数。

  4. 错误恢复:如果批量更新失败,会恢复之前的点赞数,确保数据一致性。

这种方式的优点是通过批量处理来减少数据库的压力,尤其是在高并发场景下,能够有效地减少频繁的数据库操作,提高系统性能。

接下来我们就可以编写我们的点赞的服务了:

import { Injectable } from "@nestjs/common";
import { AmqpConnection } from "@golevelup/nestjs-rabbitmq";

@Injectable()
export class LikeProducerService {
  constructor(private readonly amqpConnection: AmqpConnection) {}

  async sendLike(userId: string, postId: string) {
    try {
      await this.amqpConnection.publish("like_exchange", "like", {
        userId,
        postId,
      });
      console.log(`用户 ${userId} 对帖子 ${postId} 发送了点赞消息`);
    } catch (error) {
      console.error("发送点赞消息失败:", error);
    }
  }
}

在上面的这段代码里,主要还是这段代码:

await this.amqpConnection.publish("like_exchange", "like", {
  userId,
  postId,
});

这行代码的作用是向 like_exchange 交换机发布一条消息,消息的路由键是 like,消息内容包含 userId 和 postId。这条消息会被 RabbitMQ 路由到与 like 路由键匹配的队列,供消费者(如 LikeConsumerService)处理。

接下来我们要访问路由来对该接口模拟一个频繁的点赞效果:

20241113155326

如下输出所示,我们多次点赞,最终只操作了两次的数据库,如果再优化一下就符合我们的削峰要求了:

20241113155454

总结

在 NestJS 项目中结合 RabbitMQ 实现点赞系统的削峰功能,通过将点赞请求异步发送到消息队列,解耦了高并发情况下的点赞操作。利用 @golevelup/nestjs-rabbitmq 库,生产者将点赞消息推送到 RabbitMQ 交换机,而消费者则批量处理点赞数的更新,减少了频繁的数据库写入操作。通过使用 RxJS 的 bufferTime 操作符,点赞数据每 5 秒批量更新一次,避免了高并发时的数据库竞争和性能瓶颈,从而提升了系统的稳定性和处理效率。这种方式有效地减轻了数据库的压力,确保了高并发环境下点赞数的准确性和一致性。

最后再来提一下这两个开源项目,它们都是我们目前正在维护的开源项目:

如果你想参与进来开发或者想进群学习,可以添加我微信 yunmz777,后面还会有很多需求,等这个项目完成之后还会有很多新的并且很有趣的开源项目等着你。