4、工作队列模式:轮询分发、消息确认、消息持久化、公平分发

工作队列模式,说白了就是「把任务分给多个工人去干」。

我刚开始接触 RabbitMQ 时,觉得这东西不就是个消息队列嘛,能有多复杂?直到我在一个实际项目中,因为没处理好消息确认,导致数据丢失了整整一个晚上……嗯,从那以后我再也不敢小看这几个基础特性了。

4.1 轮询分发:默认的「平均主义」

RabbitMQ 默认的分发策略就是轮询。什么意思呢?就是不管你的消费者能力强不强,每人分一个,轮流来。

举个例子,生产者发了 10 条消息,有两个消费者。那么消费者 A 拿第 1、3、5、7、9 条,消费者 B 拿第 2、4、6、8、10 条。一人一半,公平得很。

代码实现很简单:

// 生产者 - 发送消息
const amqp = require('amqplib');

async function sendTasks() {
    const connection = await amqp.connect('amqp://localhost');
    const channel = await connection.createChannel();
    const queue = 'task_queue';

    await channel.assertQueue(queue, { durable: true });

    for (let i = 0; i < 10; i++) {
        const msg = `Task ${i}`;
        channel.sendToQueue(queue, Buffer.from(msg), { persistent: true });
        console.log(`Sent: ${msg}`);
    }

    setTimeout(() => connection.close(), 500);
}

sendTasks();
// 消费者 - 接收消息
const amqp = require('amqplib');

async function receiveTask(name) {
    const connection = await amqp.connect('amqp://localhost');
    const channel = await connection.createChannel();
    const queue = 'task_queue';

    await channel.assertQueue(queue, { durable: true });
    channel.consume(queue, (msg) => {
        if (msg !== null) {
            console.log(`${name} received: ${msg.content.toString()}`);
            // 模拟处理时间
            setTimeout(() => {
                channel.ack(msg);
            }, 1000);
        }
    }, { noAck: false });
}

receiveTask('Worker A');
receiveTask('Worker B');

关键点:轮询分发是默认行为,不需要额外配置。但要注意,它不考虑消费者的实际处理能力。

4.2 消息确认:别让任务「死得不明不白」

我在项目中遇到过一个问题:消费者拿到消息后,处理到一半崩溃了。消息没了,任务也没执行。这就是典型的「消息丢失」。

解决方案就是消息确认(ACK)。消费者处理完消息后,主动告诉 RabbitMQ:「我搞定了,你可以删掉这条消息了。」如果消费者挂了,RabbitMQ 会把消息重新投递给其他消费者。

实现方式:

// 消费者 - 手动确认
channel.consume(queue, (msg) => {
    if (msg !== null) {
        try {
            // 处理消息
            console.log(`Processing: ${msg.content.toString()}`);
            // 处理完成后确认
            channel.ack(msg);
        } catch (error) {
            // 处理失败,拒绝消息
            channel.nack(msg, false, true); // 重新入队
        }
    }
}, { noAck: false }); // noAck: false 表示手动确认

避坑指南:我曾经把 noAck 设置成了 true,结果消费者一拿到消息就自动确认了。后来消费者处理到一半挂了,消息就丢了。嗯,这个坑我踩过,你们别踩。

4.3 消息持久化:重启也不怕

你想想看,如果 RabbitMQ 服务器突然重启了,队列里的消息还在吗?默认情况下,消息是存在内存里的,重启就没了。

要保证消息不丢,需要做两件事:

  1. 队列持久化:声明队列时设置 durable: true
  2. 消息持久化:发送消息时设置 persistent: true
// 声明持久化队列
await channel.assertQueue('task_queue', { durable: true });

// 发送持久化消息
channel.sendToQueue(queue, Buffer.from(msg), { persistent: true });
配置项 作用 是否必须
队列 durable: true 队列元数据持久化到磁盘
消息 persistent: true 消息内容持久化到磁盘
手动 ACK 确保消息被处理后才删除 推荐

小提示:持久化会影响性能,因为要写磁盘。如果对数据一致性要求不高,可以权衡一下是否开启。

4.4 公平分发:能者多劳

轮询分发有个问题:如果消费者 A 处理一条消息要 1 秒,消费者 B 要 5 秒。结果就是 A 闲得发慌,B 忙得要死。

公平分发(Fair Dispatch)就是来解决这个问题的。它告诉 RabbitMQ:「别一次性给消费者太多消息,等它处理完了再给新的。」

实现方式:

// 消费者 - 设置预取数量
channel.prefetch(1); // 每次只取一条消息

channel.consume(queue, (msg) => {
    if (msg !== null) {
        console.log(`Processing: ${msg.content.toString()}`);
        setTimeout(() => {
            channel.ack(msg);
        }, Math.random() * 3000); // 模拟不同处理时间
    }
}, { noAck: false });

prefetch(1) 的意思是:消费者每次只从队列里拿一条消息。处理完并确认后,才能拿下一条。这样处理快的消费者就能多干活,处理慢的消费者就少干活。

核心要点:公平分发 + 消息确认 + 消息持久化,这三者配合使用,才能构建一个可靠的工作队列。

4.5 实战建议

我个人习惯在生产环境中这样配置:

  • 队列设置 durable: true
  • 消息设置 persistent: true
  • 消费者设置 noAck: false,手动确认
  • 消费者设置 prefetch: 1,实现公平分发
  • 处理失败的消息,用 nack 重新入队,但设置重试次数限制

我记得有一次线上事故,就是因为没设置 prefetch,结果某个消费者积压了上千条消息,内存直接爆了。嗯,从那以后,prefetch 就成了我的标配。

说白了,工作队列模式的核心就四个字:可靠、公平。可靠靠的是消息确认和持久化,公平靠的是预取数量控制。把这几个点吃透了,大部分任务队列场景都能搞定。