假设我们正在开发一个博客的系统,那必然少不了点赞的需求了,但是有一个问题,当多个用户几乎同时点赞同一内容时,如果没有合理的处理机制,可能会出现以下问题:
-
数据库的写入冲突:多个用户同时对同一内容进行点赞,可能会导致数据库的竞争条件。
-
点赞数不一致:在高并发情况下,更新点赞数可能发生竞争,导致数据库中存储的点赞数与实际点赞数不一致。
-
性能瓶颈:数据库频繁更新可能会导致瓶颈,特别是当点赞量特别大的时候。
我们可以通过 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/#/ 便可以查阅相关页面:
项目集成
@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 {}
这段代码的目的是:
-
配置和初始化 RabbitMQ 连接,包括 RabbitMQ 的 URI 和交换机的设置。
-
使用 RabbitMQModule 来封装 RabbitMQ 的相关配置,使得在 NestJS 中更方便地使用 RabbitMQ 进行消息传递。
-
将 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;
}
}
}
这段代码实现了以下功能:
-
消费消息:通过 @RabbitSubscribe 装饰器监听 RabbitMQ 消息队列,接收用户的点赞信息(userId 和 postId)。
-
批量处理:使用 RxJS 的 bufferTime 操作符将收到的点赞信息按 5 秒的间隔进行批量收集,然后触发批量更新操作。这样可以减少对数据库的压力,避免频繁更新。
-
批量更新:在 5 秒内收集到的点赞数会通过 triggerBulkUpdate 方法进行批量更新,更新完成后清空缓存的点赞数。
-
错误恢复:如果批量更新失败,会恢复之前的点赞数,确保数据一致性。
这种方式的优点是通过批量处理来减少数据库的压力,尤其是在高并发场景下,能够有效地减少频繁的数据库操作,提高系统性能。
接下来我们就可以编写我们的点赞的服务了:
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)处理。
接下来我们要访问路由来对该接口模拟一个频繁的点赞效果:
如下输出所示,我们多次点赞,最终只操作了两次的数据库,如果再优化一下就符合我们的削峰要求了:
总结
在 NestJS 项目中结合 RabbitMQ 实现点赞系统的削峰功能,通过将点赞请求异步发送到消息队列,解耦了高并发情况下的点赞操作。利用 @golevelup/nestjs-rabbitmq 库,生产者将点赞消息推送到 RabbitMQ 交换机,而消费者则批量处理点赞数的更新,减少了频繁的数据库写入操作。通过使用 RxJS 的 bufferTime 操作符,点赞数据每 5 秒批量更新一次,避免了高并发时的数据库竞争和性能瓶颈,从而提升了系统的稳定性和处理效率。这种方式有效地减轻了数据库的压力,确保了高并发环境下点赞数的准确性和一致性。
最后再来提一下这两个开源项目,它们都是我们目前正在维护的开源项目:
如果你想参与进来开发或者想进群学习,可以添加我微信 yunmz777,后面还会有很多需求,等这个项目完成之后还会有很多新的并且很有趣的开源项目等着你。