RabbitMQ使用教程(三):任务分发模式—Work

一、什么是Work模式

在入门程序中,我们是使用的一个生产者,一个消费者。试想:如果有几个消息都需要处理,且每个消息的处理时间很长,仅有一个消费者,那么当它在处理一个消息的时候,其他消息就只有等待。

等待有时候是好的,但在程序中并不那么好,当队列中有多个消息待处理,将其分发给多个消费者,当一个消费者在处理的时候,有其他消费者继续消费队列中的消息,便缓解了等待的尴尬。

那么这篇文章将实现一个生产者,多个消费者的模式,实现任务分发:work模式,如图所示。

这里写图片描述

这样看来,我们写的入门程序其实就是Work模式的一个特例:只有一个Worker。

要更好的了解work模式,我们需要知道RabbitMQ的一些机制,以下就从问题出发一个个的给出了解释。

二、消息确认机制
问题:怎样保证消息不因消费者gg而丢失

处理一个消息可能会花一定的时间,万一还没处理完消费者就gg了…生产者一发送消息,便会将其标记为已删除,故最终的结果是:这条消息没有得到正确的处理。而且,指派给该消费者且尚未处理的所有消息都会gg。

解决策略:取消自动回复机制

为了解决消息的丢失问题,RabbitMQ提供了消息确认机制:message acknowledgments,一个消费者处理完成后,将会回传一个ack给生产者,以表示处理成功,这样生产者才可以将消息删除。

这样即使一个消费者gg了,没有回传ack,那么发送者便会重发消息到队列,如果这时候有其他的消费者服务该队列,那么便会从队列中取出消息并处理。这就保证了消息的不丢失。

自动回复机制:不管是否处理成功,还是失败,都会回复ack。

  channel.basicConsume(QUEUE_NAME, true, consumer);

自动恢复机制默认是打开的,在接收端的代码最后:第二个参数为true,表示会自动回复,只要生产发送消息,就会标记删除。所以我们需要将自动回复设置为false。

  boolean autoAck = false;
  channel.basicConsume(QUEUE_NAME, autoAck, consumer);

这样来保证消息不会因为消费者的gg而丢失了。

那么取消自动回复以后,我们需要手动回复一次:

 channel.basicAck(envelope.getDeliveryTag(), false);

注意当前的消息确认机制只适用于同一个channel。

三、消息持久化
问题:怎样保证消息不因生产者gg而丢失

我们知道了如何在消费者的角度保证消息不丢失,但如果生产者gg了呢,消息同样会丢失,生产者gg后会默认丢弃所有的消息,除非告诉它某些消息是不能丢失的。

解决策略:消息持久化

使用消息持久化,将消息保存到磁盘上,而不是内存中,即使生产者gg了,后面还可以通过读取磁盘来进行恢复。

要实现消息持久化,我们需要做两件事:从queue与message分别来标记持久化。

①首先:从queue角度标记为持久化

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

声明队列时的第二个参数,设置为true。当然以上代码是有问题的,因为我们已经声明一个hello了,而且那个hello的持久化是false的,这里我们需要声明一个新的队列:queue_task

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

②从message的角度标记持久化
我们已经标记了queue为持久化,重启后会读取磁盘保存的消息,那么还需要将消息标记为持久化:通过设置MessageProperties的值为:PERSISTENT_TEXT_PLAIN

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

好了现在我们已经实现消息持久化了。

注意:消息持久化并不能完全保证消息不丢失,级生产者需要将多个message保存到磁盘上,就在保存这个时间窗口上发生了意外,消息同样会丢失,尽管这个时间很短,但还是存在。不过话说回来,尽管这个持久化机制不能百分百地保证消息不丢失,但是做一些简单的任务还是够用的。
四、负载均衡
问题:怎样实现消息的均匀分配

你可能意识到了我们的任务分发模式还不是我们想要的,举个例子,一些消息处理时间长,一些消息处理时间短,当我们把一个任务重的消息发送给了一个worker1,把一个任务轻的消息发送给了worker2,现在又来了两个消息,我们又把任务重的分给了worker1,轻的分给了worker2,这样worker1的任务就相当的重,而worker就会很闲。当然这不是我们想要的。

解决策略:单个处理原则

采用单个处理原则,我们每次都只分发一次任务给消费者,换句话说,如果一个消费者尚未处理完成,RabbitMQ不会分配新的消息给消费者。如图所示
这里写图片描述
代码实现:

int prefetchCount = 1;
channel.basicQos(prefetchCount );// 负载均衡
注意:如果所有的worker都在处理,queue可能出现full的情况,这是需要监控的,或者通过其他策略来调整的。
五、Work模式代码实现

我们使用Thread.sleep()模拟消息处理需要时间。

生产者,我们用多任务NewTask命名:


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

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

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

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

        for (int i = 1; i <= 10; i++) {
            String message = "the message" + i;
            //标记message持久化
            channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
        }
        channel.close();
        connection.close();

    }

}

消费者1-Work1


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

import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

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

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();
        //标记queue持久化
        boolean durable = true;
        channel.queueDeclareNoWait(QUEUE_NAME, durable, false, false, null);
        channel.basicQos(1);// 负载均衡
        final Consumer consumer = new DefaultConsumer(channel) {

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
                    throws IOException {
                String message = new String(body, "UTF-8");
                try {
                    doWork(message);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //手动回复一次
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            }
        };
        boolean autoAck = false;// 自动回复确认,默认为true,生产者已发送就会将其标记为:已删除
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }

    protected static void doWork(String message) throws InterruptedException {
        Thread.sleep(1000);
        System.out.println("work1收到消息:" + message);
    }
}

消费者2-Work2,与Work1代码一模一样,除了涉及到的名字不同。

运行与结果:

①首先运行NewTask,再运行Work1(不运行Work2)
这里写图片描述
②登录管理端,删除task_queue
这里写图片描述
这里写图片描述
首先运行NewTask,再运行Work1,再运行Work2
这里写图片描述
可以看到Work2处理了消息:4、6、8、10,没有处理2是因为我们只运行Work1与Work2的时间间隔中,Work1多运行了一些。而且由于console的覆盖,我们也看不到Work1的打印了,如果要完全在控制台看到平均的处理结果,可以使用多个ide运行。

下一篇我们将一起来探索:发布/订阅 模式

猜你喜欢

转载自blog.csdn.net/qq_35890572/article/details/81869308