RabbitMQ(二)——工作队列,分发消息到多个消费者

前言

在上一篇博客中,简单介绍了MQ,并且建立了一个queue用于消息发送,这一篇博客的实例中我们将会建立一个队列,这个队列将会将待消费的任务发送给多个消费者。

工作队列的主要作用,就是为了避免出现同步等待执行资源密集型任务的情况,为了支持异步调用,我们将任务封装成消息(message),然后将其送入队列。消费者线程会在后台获取该消息并最终消费掉该消息,如果有多个消费者,这个消息将会被这些消费者通消费(这一段来自官网,自我翻译的,原文如下: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. We encapsulate a task as a message and send it to a queue. A worker process running in the background will pop the tasks and eventually execute the job. When you run many workers the tasks will be shared between them.)。

准备

在进行该篇博客的实例之前,需要做一些准备工作。上一篇博客中我们简单发送了一个Hello world的message,但是这一篇博客中,我们将会发送一个字符串,消费者根据这个字符串的内容,模拟处理耗时较久的程序。官网根据传入的应用程序中的字符串末尾点的个数来决定消费者处理的耗时长短,这里我们为了显示明显决定修改一下官网的实例。先贴出消费者耗时处理的逻辑

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("message deal done");
    }
};

doWork的逻辑代码——根据message中得到的字符串的最后一个数字大小控制耗时长短。

private static void doWork(String message) throws InterruptedException {
    int times = Integer.valueOf(message.charAt(message.length() - 1));
    System.out.println("消费者处理"+times*2000+"毫秒");
    Thread.sleep(1000 * times);
}

MQ的消息确认机制

当多个消费者消费同一个队列的时候,MQ会采用轮询发送消息的机制,这个其实比较简单,就不展开说明了。具体参见官网就可以,这里主要总结一下消息确认机制。

通常情况下消费者处理一个消息至少需要耗费几秒,我们可能会好奇,如果一个消费者处理耗时较长,或者在处理消息的中途挂掉了将会发生什么。在我们上一篇博客中的代码,如果任何一个消费者出现处理消息耗时过长,或者在处理消息的中途挂掉则消息会被删除,同时RabbitMQ也会删除发送给该消费者的消息。

但是RabbitMQ不想让任何一个消息丢失,如果处理这条消息的消费者在毫无征兆的情况下挂掉了,RabbitMQ应该将这条没有正常处理的消息发送给其他消费者处理。为了确保消息不丢失,RabbitMQ支持消息确认机制——这种确认消息是由消费者发送给RabbitMQ,告知RabbitMQ该消息已经被消费,RabbitMQ可以将这个消息删除了。

如果消费者没有发送确认的消息,则RabbitMQ会认为这个消息没有正确消费,会将这个消息重新入队。然后将这个消息转发给其他消费者。

这里还是说一下RabbitMQ中的autoAck参数,官网中有这样一段话

这段话的意思就是,手动确认消息机制是默认开启的,如果autoAck(channel.basicConsume的第二个参数)设置为false,则手动确认消息机制将会关闭。当时看到这里有一丝疑惑,难道还分自动确认和手动确认?为什么得到消费者的回执需要关闭自动确认,直接开启自动确认不是更加省事么?后来我看到了下面一段话才恍然大悟:

 RabbitMQ还真有两种消息确认机制,客户端告知RabbitMQ一条确认的消息被消费了这种即为手动确认机制,如果RabbitMQ将消息从队列中发出后立即给予确认,这种即为自动确认机制,RabbitMQ默认开启了第二种确认机制。

为了演示消息分发的机制,我们需要将autoAck属性置为false,关闭自动确认机制

于是我们消费者的代码如下:

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

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

/**
 * autor:liman
 * createtime:2019/10/10
 * comment:
 */
public class Worker {

    private static final String QUEUE_NAME = "hello_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

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

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
            try{
                doWork(message);
            }catch (Exception e){

            }finally {
                System.out.println("done");
            }
        };

        //自动确认默认是开启的,这里设置成false
        boolean autoAck = false;
        //正式消费消息,指定消费消息的回调内部类
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
    }

    private static void doWork(String message) throws InterruptedException {
        int times = Integer.valueOf(String.valueOf(message.charAt(message.length() - 1)));
        System.out.println("消费者处理"+times*2000+"毫秒");
        Thread.sleep(times*2000);
    }

}

这里设置了autoAck为false,关闭了自动确认消息机制,改为手动,手动确认机制,该模式下如果消费者没有发送回执,则RabbitMQ则会直接将未消费的消息重新入队。

测试

启动两个消费者程序。之后再启动生产者。

生产者发送的消息列表

 后面的一串数值用于标记消息编码。两个消费者的代码如下:

消费者1运行结果

 消费者2运行结果:

可以看出消息分发均匀,这里就是RabbitMQ轮询的结果。

之后将消费者代码线程sleep的时间设置长一点。以同样的方式执行代码(启动多个消费者,一个生产者)

autoAck置为false的情况下

启动消费者后,迅速停止线程,通过RabbitMQ控制台会发现存在消息堆积的情况

 在autoAck置为true的时候,启动消费者之后,迅速停止消费者线程,并不会有堆积的消息。

之后重新启动消费者,消息会被正常消费。

消息持久化

我们已经知道了消费者如果没有正确消费消息该如何处理,但是如果RabbitMQ服务器挂掉了怎么办?

当RabbitMQ服务器挂掉了,服务器中的消息就不存在了,除非我们进行了消息的持久化。为了确保消息不丢失,我们需要对队列和消息都进行持久化。

队列的持久化在我们进行队列声明的时候设置durable参数,

boolean durable = true;
//第二个参数设置为true,表示消息队列要进行持久化
channel.queueDeclare(QUEUE_NAME,durable ,false,false,null);

但是如果直接在上面的代码中修改属性,并不会生效。如果我们启动会抛出以下异常

 这是因为我们之前已经建立了一个hello_queue的队列,RabbitMQ不允许建立两个相同名字但是参数不同的队列

公平的消息分发机制

上述代码在实际运行的时候,我们已经看到了轮询机制的作用,但是对于时间上的分发并没有那么公平,收到消息文本字符为偶数的消费者处理消息的任务耗时明显比另一个消费者多,这似乎有点不公平。出现这个问题的原因是,上述代码中RabbitMQ并没有查看客户端未确认消息的个数。(This happens because RabbitMQ just dispatches a message when the message enters the queue. It doesn't look at the number of unacknowledged messages for a consumer.)RabbitMQ只是进行了简单的轮询。我们可以用basicQos方法解决上述问题。其中的prefetchCount参数,这个就是告知如果有prefetchCount个消息没有返回确认则暂缓将剩下的消息发送给指定的消费者。

直接在消费中加入下面两行代码即可体验上述结果:

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

总结:

1、多个消费者监听同一个队列,RabbitMQ会进行消息的轮询分发。

2、消息确认机制中,channel.basicConsume方法中的第二个参数设置消息回复机制,RabbitMQ的消息回复机制有两种

3、队列持久化是为了确保消息在队列中不被丢失,channel.queueDeclare中的第二个参数就是设置消息在队列中的持久化属性

4、channel.basicQos方法指定消费者未确认的消息个数(这里介绍的不太清楚,可以直接参见官网)

参考资料:

Distributing tasks among workers (the competing consumers pattern)

发布了129 篇原创文章 · 获赞 37 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/102520614