RabbitMQ——3、工作队列WorkQueue

本文详细介绍了RabbitMQ的工作队列原理,包括循环分配、消息确认、持久化、公平分发等关键概念,并提供了代码示例,帮助读者深入理解如何使用RabbitMQ实现任务队列。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概述

The main idea behind Work Queues (aka: Task Queues) is to avoid doing a resource-intensive task immediately and having to wait for it to complete.Instead we schedule the task to be done later. 工作队列(即任务队列)背后的主要思想是避免立即执行资源密集型任务,并且必须等待它完成。相反,我们将工作安排在稍后执行。

工作队列的概念对WEB应用尤其有用,因为往往很难在短时间的HTTP请求窗口中完成复杂的任务。工作队列的优点之一是更轻松地处理并行工作。如果我们的工作快速积压,那么可以增加更多的工人,这样,规模就很容易扩大。当队列中有很多耗时任务时,增加多个消费者可以分摊这些任务,从而提高并发性能。

二、循环分配(Round-robin dispatching)

2.1 概念

默认情况下,RabbitMQ将按顺序将每个消息发送给下一个使用者。

2.2 实例

实例源码参考官方教程:
https://www.rabbitmq.com/tutorials/tutorial-two-java.html

运行测试结果如下:

Worker1:

[*] Waiting for messages. To exit press CTRL+C
 [x] Received 'First message.'
 [x] Done
 [x] Received 'Third message...'
 [x] Done

Worker2:

[*] Waiting for messages. To exit press CTRL+C
 [x] Received ''
 [x] Done
 [x] Received 'Second message..'
 [x] Done

三、消息确认(message acknowledgment)

做任务需要花费时间,如果其中一个消费者启动了一个很长的任务,但只完成了一部分就挂掉了,会发生什么情况?

在现阶段的代码中,一旦worker接收到了消息,rabbitmq会立即标记该消息为已处理。如果此时worker挂掉了,那么该消息就将会丢失。

这种情形显然是不能别接受的,我们希望的是,如果一个worker挂掉了,那么该任务会被投递到其他worker继续完成。

为了避免消息丢失,RabbitMQ提供消息确认机制,当消息被接受、处理后,消费者会给RabbitMQ发送一个ACK,让RabbitMQ可以安心删除该消息。如果消费者挂掉了,没有返回ACK,那么RabbitMQ就会将该任务重新装入队列并分配给其他空闲的消费者。这样,即使有消费者偶尔挂掉了,也可以确保没有任何消息会丢失。

同时,由于没有任何消息超时时间,所以即使一项任务需要花费很长很长的时间也没有关系。

默认情况下,将打开手动消息确认。在前面的示例中,我们通过autoAck=true标志显式地关闭了它们。我们可以将该标志设置为false,并从worker发送一个适当的确认。

        DeliverCallback deliverCallback = (consumerTag,delivery)->{
            String message = new String(delivery.getBody(), "UTF-8");

            System.out.println(" [x] Received '" + message + "'");
            try {
                doWork(message);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(" [x] Done");
                //2.手动确认,如果该进程中途挂掉,那么消息会被重新投递到另一个消费者
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };

        boolean autoAck = false; // 1.关闭自动确认功能
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });

确认必须和接受消息的消费者使用同一个channel,否则会产生一个channel级别的protocol 异常。

漏掉ACK是一个常见的错误,但它的后果很严重。当该消费者退出时,所有曾发送给它的消息都将会被再次投递!!!并且RabbitMQ将消耗越来越多的内存,因为它将无法释放任何未加密的消息。

四、持久性durable

使用ack机制可以确保消费者挂掉时不会丢失消息,那么当RabbitMQ Server挂掉的时候呢,又该怎样避免消息丢失呢?

4.1 队列的持久性

当RabbitMQ Server退出或者挂掉时,它会丢掉所有的队列和消息,除非我们明确地给队列和消息打上durable的标记。示例如下:

boolean durable = true;
channel.queueDeclare("workQueue", durable, false, false, null);

注意,RabbitMQ不允许重新定义队列的持久化属性,也就是说durable参数只对新创建的队列有效。

当我们将durable属性设置为true时,即使RabbitMQ Server重启了,workQueue队列也不会丢失.

注意,queueDeclare在生产者和消费者中都要声明durable属性为true。

4.2 消息的持久性

队列通过durable属性进行持久化,那么消息呢?消息是通过将MessageProperties设置为PERSISTENT_TEXT_PLAIN实现的:

import com.rabbitmq.client.MessageProperties;

channel.basicPublish("", "task_queue",
            MessageProperties.PERSISTENT_TEXT_PLAIN,
            message.getBytes());

该标志告诉RabbitMQ将本条消息存储到磁盘中,以达到持久化的目的。但该措施并不能百分百保证持久化,因为在收到消息和存储到磁盘这个过程中有一个时间窗口,如果在该窗口中Server挂掉了,那么消息也会丢失。如果需要更严格的保证,那么可以使用publisher confirms。

五、公平分发Fair Dispatch

5.1 问题

到目前为止,如果有多个消费者,我们的分发机制是轮流分发。这种机制存在一些问题,例如所有排在奇数位置的都是重任务、在偶数的都是轻任务,这样就会导致其中一个Worker一直处于繁忙状态。

这是因为RabbitMQ只在消息进入队列时进行分发,它不管上一条消息是否已经处理完毕(收到ack),只是盲目地将第n条消息发送给地n个消费者。

5.2 解决方案——basicQos

我们可以通过basicQos方法和prefetchCount=1来告诉RabbitMQ,不要同时给一个发消费者发送多条消息,即只有在消费者已经处理完并回复ack后再为其分配下一条消息。这样,就能将消费者资源最大化利用,谁空闲,谁处理,实现相对公平的分发。

Worker.java(消费者)

int prefetchCount=1;
channel.basicQos(prefetchCount);

六、整体代码

NewTask.java(生产者)

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class NewTask {
    private final static String QUEUE_NAME="task_queue";

    public static void main(String[] args) {
        //1 实例化连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("admin");
        factory.setPassword("admin");
        //factory.setHost("www.deardull.com");
        try {
            //2 创建连接
            Connection conn = factory.newConnection();
            //3 创建channel.接下来,我们创建一个通道,用于完成任务的大部分API都位于这个通道中。
            Channel channel = conn.createChannel();
            //4 声明队列,发送的消息将送到该队列中
            boolean durable=true; //队列持久化
            channel.queueDeclare(QUEUE_NAME,durable,false,false,null);
            //5 发送消息
            String message = String.join(" ", args);
            //MessageProperties.PERSISTENT_TEXT_PLAIN 消息持久化
            channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
            System.out.println("[send]"+message);

        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

Worker.java(消费者)

public class Worker {
    private final static String QUEUE_NAME = "task_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //1 连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //2 设置连接属性
        factory.setHost("127.0.0.1");
        factory.setUsername("admin");
        factory.setPassword("admin");
        //3 获取连接
        Connection conn = factory.newConnection();
        //4 获取channel
        Channel channel = conn.createChannel();
        //5 为通道绑定队列
        boolean durable = true;  //持久化属性
        channel.queueDeclare(QUEUE_NAME,durable,false,false,null);
        //6 每次只接收一条消息,处理完再接收下一条
        channel.basicQos(1);

        System.out.println("[*] Waiting for messages. To exit press CTRL+C");

        //7 定义回调函数
        DeliverCallback deliverCallback = (consumerTag,delivery)->{
            String message = new String(delivery.getBody(), "UTF-8");

            System.out.println(" [x] Received '" + message + "'");
            try {
                doWork(message);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(" [x] Done");
                //手动确认,如果该进程中途挂掉,那么消息会被重新投递到另一个消费者
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };

        //8 消费消息
        boolean autoAck = false; // 关闭自动确认功能
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });

    }

    private static void doWork(String task) throws InterruptedException {
        for (char ch: task.toCharArray()) {
            if (ch == '.') Thread.sleep(1000);
        }
    }
}

七、参考文档

参考RabbitMQ官方文档:https://www.rabbitmq.com/tutorials/tutorial-two-java.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值