实现动机
起因是在项目中有一个发布作品的功能,而我们需要对发布的内容进行审核,最开始审核的内容不算多所以同事直接在后端的发布逻辑中直接加上了审核,但随着需求的增加显然这个逻辑就不那么可靠了。在忙完其他任务之后,就想着来做这个优化。
配置环境
我的网站部署在宝塔面板上,所以需要先在宝塔中配置一下RabbitMQ的运行环境。哦对了!开始前记得到安全组把自己的5672和15672端口放行,分别是RabbitMQ和它的Web面板运行的地方。
Erlang/OTP 安装
首先我去看了一下宝塔软件商城提供的rabbitmq管理器是3.12.4版本,根据文档去查看了一下对应需要的Erlang/OTP版本,我这里用的版本是Erlang/OTP 26.0.2,可以在本地先下载后放到宝塔上(我是这么做)。
放到root目录下后,到终端操作:
解压源码
# 解压并进入目录
tar -zxvf otp_src_26.0.2.tar.gz
cd otp_src_26.0.2
配置编译选项
# 指定安装路径为 /usr/local/erlang-26.0.2
./configure \
--prefix=/usr/local/erlang-26.0.2 \
--with-ssl \
--enable-threads \
--enable-smp-support \
--enable-kernel-poll \
--enable-hipe
编译并安装
# 多线程编译(根据 CPU 核心数调整)
make -j$(nproc)
sudo make install
# 添加环境变量
echo 'export PATH=/usr/local/erlang-26.0.2/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
# 验证安装
erl -version # 应输出 "Erlang (SMP,ASYNC_THREADS) (BEAM) emulator version 14.0.2"
RabbitMQ 安装
一样可以在本地安装后上传到宝塔文件里,因为我直接在宝塔里安装太慢了。QAQ
解压rabbitmq
# 解压到 /usr/local
sudo tar -xf rabbitmq-server-generic-unix-3.12.10.tar.xz -C /usr/local/
sudo mv /usr/local/rabbitmq_server-3.12.10 /usr/local/rabbitmq
# 添加环境变量
echo 'export PATH=/usr/local/rabbitmq/sbin:$PATH' >> ~/.bashrc
source ~/.bashrc
到这里时,我去检查了文件路径等都没有问题,但当我执行sudo rabbitmq-plugins enable rabbitmq_management
的时候,却遇到报错了sudo: rabbitmq-plugins: command not found
。于是我就试着将它加入系统变量:
将路径加入系统变量
# 1. 编辑环境变量配置文件
echo 'export PATH=$PATH:/usr/local/rabbitmq/sbin' | sudo tee -a /etc/profile
# 2. 立即生效
source /etc/profile
# 3. 验证是否生效
which rabbitmq-plugins # 应输出路径,例如 /usr/local/rabbitmq/sbin/rabbitmq-plugins
当然执行时还是会提示,但是我发现不加sudo
居然就可以了,于是:
启动服务并启用管理插件
# 启用管理插件
rabbitmq-plugins enable rabbitmq_management
# 启动服务(后台模式)
rabbitmq-server -detached
# 验证服务状态
rabbitmqctl status
这样就可以看到自己的RabbitMQ是否运行啦,如果觉得不直观可以使用这个命令来查看端口是否被监听sudo ss -tulnp | grep -E '5672|15672'
,如果后面有users(("beam.smp",pid=xxxx))
就可行啦。
最后可以配置Systemd 服务实现开机自启: 在以下路径创建一个服务文件/etc/systemd/system/rabbitmq.service
,内容如下:
内容
[Unit]
Description=RabbitMQ Server
After=network.target
[Service]
Type=forking
User=root
ExecStart=/usr/local/rabbitmq/sbin/rabbitmq-server -detached
ExecStop=/usr/local/rabbitmq/sbin/rabbitmqctl stop
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
启动并设置开机自启
# 重载 Systemd
sudo systemctl daemon-reload
# 启动服务
sudo systemctl start rabbitmq
# 设置开机自启
sudo systemctl enable rabbitmq
# 检查状态
sudo systemctl status rabbitmq
使用Node.js实现消费者
当我们开始运行RabbitMQ后,就可以来实现消费者连接它啦。这里我使用的语言是Node.js:因为我自己比较熟悉,用了好几年了,而且我的使用场景刚好是属于I/O密集型(需要频繁发请求)的任务、Node.js 是轻量级、高并发场景的优选。这里我也咨询了AI,给出几种常用语言的优劣势:
1. Node.js
适用场景:
- 高并发 I/O 密集型任务:例如实时消息处理、API 网关、微服务间通信。
- 轻量级服务:如 Serverless 或无状态消费者。
- 与前端技术栈统一:若团队熟悉 JavaScript/TypeScript,可减少上下文切换成本。
优势:
- 非阻塞异步模型:基于事件循环,能高效处理大量并发消息,避免线程阻塞。
- 低资源消耗:相比 Java 或 C#,内存占用更小,启动更快。
- 生态丰富 :通过
amqplib
或rascal
等库快速集成 RabbitMQ,支持 Promise 和 async/await。 - 适合微服务架构:与 Express、NestJS 等框架无缝协作,适合构建轻量级服务。
劣势:
- 单线程限制:CPU 密集型任务(如复杂计算)可能阻塞事件循环,需谨慎处理。
- 回调地狱:需通过 async/await 或 Promise 链式调用管理异步逻辑。
2. Python
优势:
- 语法简洁,开发速度快,适合快速原型。
- 库支持完善(如
pika
、celery
)。
劣势:
- 全局解释器锁(GIL)限制多线程并发性能。
- 性能低于编译型语言,适合低频任务。
3. Go (Golang)
优势:
- 原生并发支持(goroutine + channel),适合高吞吐场景。
- 编译为静态二进制文件,部署简单。
- 内存占用低,性能接近 C/C++。
劣势:
- 语法和错误处理需要适应(显式 error 检查)。
- 生态库(如
streadway/amqp
)功能较基础。
4. Java
优势:
- 强大的并发处理(线程池、NIO),适合企业级高负载场景。
- 成熟生态(Spring AMQP、RabbitMQ Client)。
- 类型安全和 JVM 优化,适合复杂业务逻辑。
劣势:
- 启动时间较长,内存消耗高。
- 代码冗余度高,开发效率较低。
接下来,上代码:
js
const express = require('express');
const amqp = require('amqplib');
const redis = require('redis');
const mysql = require('mysql2/promise');
const WebSocket = require('ws');
const axios = require('axios');
const os = require('os')
const URLSearchParams = require('url').URLSearchParams;
// 写一个函数用于判断当前的ip地址:我用来区分开发环境、测试环境、生产环境
const getLocalIP = () => {
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
// 跳过IPv6和内部地址
if (iface.family === 'IPv4' && !iface.internal) {
return iface.address;
}
}
}
return '0.0.0.0';
}
// 初始化 Express
const app = express();
// 全局资源初始化
let dbPool //数据库连接池
let redisClient //redis客户端
let access_token //百度智能云access_token
let env = getLocalIP() === 'xxx.xx.xxx.xxx' ? 'online' : 'test'
//自定义,组装自己的redis_key
const makeRedisKey = (ownKey) => {
return `xxxx_${ownKey}${env}.xxxx_xxxx`
}
// 添加全局WebSocket客户端变量和心跳定时器
let wsClient;
let heartbeatInterval;
const HEARTBEAT_INTERVAL = 30000; // 30秒发送一次心跳
const RECONNECT_DELAY = 5000; // 5秒后重连
//声明数据库和redis连接函数
async function initResources() {
console.log('连接数据库...')
// MySQL 连接池(每个进程独立)
dbPool = mysql.createPool({
host: 'xxx.xx.xxx.xxx',
user: 'db_username',
password: 'db_password',
database: 'db_name',
connectionLimit: 10, // 限制最大空闲连接数
idleTimeout: 10000, // 空闲连接自动关闭时间 10s
});
console.log('数据库连接成功')
console.log('连接Redis...')
// Redis 客户端(单例)
redisClient = redis.createClient({ url: 'redis://127.0.0.1:6379', password: 'password' }); //线上环境
const con = await redisClient.connect();
console.log('Redis连接成功')
}
//声明WebSocket连接函数
async function connectWebSocket() {
const socket_id = '54321'; // 替换为实际的socket_id
const deviceId = '54321'; // 替换为实际的device_id
console.log('连接WebSocket...')
try {
// 根据环境填写地址
wsClient = new WebSocket(`wss://${env?'xxx':'xxx'}/wss/?user_id=${socket_id}&device_id=${deviceId}`);
wsClient.on('open', () => {
console.log('WebSocket连接成功');
// 连接成功后启动心跳
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
heartbeatInterval = setInterval(() => {
if (wsClient && wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify({ type: 'heartbeat', timestamp: Date.now() }));
console.log('发送心跳...');
}
}, HEARTBEAT_INTERVAL);
if (wsClient && wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify({ type: 'notice', timer: Date.now(), receiver_id: '1234', sender_id: '12345', content: '测试消息' }));
}
});
console.log('WebSocket连接成功')
wsClient.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
if (message.type === 'heartbeat_response') {
console.log('收到心跳响应');
} else {
console.log('收到WebSocket消息:', data.toString());
}
} catch (error) {
console.log('收到WebSocket消息:', data.toString());
}
});
wsClient.on('error', (error) => {
console.error('WebSocket错误:', error);
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
setTimeout(connectWebSocket, RECONNECT_DELAY);
});
wsClient.on('close', () => {
console.log('WebSocket连接关闭,准备重连...');
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
setTimeout(connectWebSocket, RECONNECT_DELAY);
});
} catch (error) {
console.error('WebSocket连接失败:', error);
setTimeout(connectWebSocket, RECONNECT_DELAY);
}
}
// 这里定义了一个函数 用来模拟阻塞,平时测试的时候用到
function syncDelay(ms) {
const start = Date.now();
while (Date.now() - start < ms) { }
}
// 通过WebSocket发送消息
function sendSocketMsg(socket_id, content) {
if (wsClient && wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify({ type: 'notice', timer: Date.now(), receiver_id: socket_id, sender_id: '666', content, isreaded: 0 }));
}
}
//声明连接RabbitMQ的函数
async function connectRabbitMQ() {
console.log('连接RabbitMQ...')
// 这里填你自己的账号密码
const mq_conn = await amqp.connect('amqp://username:password@localhost:5672', {
heartbeat: 60 // 60 秒心跳,防止连接超时
});
const channel = await mq_conn.createChannel();
console.log('RabbitMQ连接成功')
//声明交换机、队列、锁 不理解可以看我上一篇文章哟~
const exchange = 'exchange_name';
const queue = 'queue_name';
const routingKey = 'route_name';
await channel.assertExchange(exchange, 'direct', { durable: true });
await channel.assertQueue(queue, { durable: true });
await channel.bindQueue(queue, exchange, routingKey);
// 设置通道数 prefetch 为 10
await channel.prefetch(10);
channel.consume(queue, async (msg) => {
if (!msg) {
channel.nack(msg, false, false)
return;
}
//取出一个数据库连接来使用
const conn = await dbPool.getConnection();
try {
// 拿到你自己存入的消息
const task = JSON.parse(msg.content.toString());
// 之后就可以做相应的审核的处理啦 用axios来发送网络请求
// 数据库拿数据
const data = await conn.execute(`
SELECT *
FROM xxx_table
WHERE xxxx
`);
// 设置redis_key 并 更新 Redis
const redisTitle = makeRedisKey(`examine_progress_${skillId}`)
await redisClient.set(redisTitle, JSON.stringify({ msg: `审核进度:100%` }), { 'EX': 3600 })
// 很关键,处理完后一定要 确认消息
channel.ack(msg);
return
} catch (err) {
// 很关键,如果处理失败 使用nack这样使用可以把信息再放回队列中
console.error(`处理失败: ${err.message}`);
channel.nack(msg,false,true);
} finally {
// 很关键,最后一定要 释放掉这个数据库连接,否则会导致数据库连接超载
conn.release();
}
});
}
//获取百度access_token
async function getAccessToken() {
console.log('获取accessToken...')
access_token = await redisClient.get('baidu_access_token');
console.log({ access_token })
//5分钟内重复不重复获取
if (access_token) return;
const options = {
'method': 'POST',
'url': 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=your_id&client_secret=your_secret',
'headers': {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
};
await axios(options).then(async res => {
access_token = res.data.access_token
console.log('获取accessToken成功')
//缓存access_token 5分钟
await redisClient.set(`baidu_access_token`, access_token, 'EX', 300000);
}).catch(error => {
console.log('获取accessToken失败', error)
})
setInterval(async () => {
await axios(options).then(async res => {
access_token = res.data.access_token
console.log('更新accessToken成功')
//缓存access_token 5分钟
await redisClient.set(`baidu_access_token`, access_token, 'EX', 300000);
}).catch(error => {
console.log('更新accessToken失败', error)
})
}, 280000)
}
async function start() {
console.log('开始咯!')
await initResources()
await connectWebSocket() // 先连接WebSocket
await getAccessToken() //再查询百度access token
app.listen(4000, () => console.log('启动进程,消息等待中...'));
await connectRabbitMQ(); // 再连接RabbitMQ
}
start().catch(err => {
console.error('启动失败:', err);
process.exit(1);
});
最后呢,就是如何在宝塔上持续运行它了。非常简单,我们使用PM2来管理进程就行。在宝塔中安装好PM2之后就可以在"软件商城"安装"PM2管理器"用来管理你的node.js项目,不需要后续的代码,下面把全部写出来:
PM2 基本使用示例
1. 安装 PM2
npm install pm2 -g
2. 启动应用
bash
pm2 start app.js # 启动单个应用
pm2 start ecosystem.config.js # 通过配置文件启动多应用
3. 常用命令
perl
pm2 list # 查看运行中的进程列表
pm2 stop app_name # 停止指定应用
pm2 restart app_name # 重启应用
pm2 delete app_name # 删除应用
pm2 reload app_name # 零停机热更新(集群模式)
这样就可以在宝塔上运行我们的消费者啦!