RabbitMQ (二) 任务队列
[外链图片转存失败(img-xh2uCA5N-1568882407041)(http://wiki.jikexueyuan.com/project/rabbitmq/images/7.png)]
在第一篇文章中,我们已经写了一个从已知队列中发送和获取消息的程序。在这篇教程中,我们将创建一个工作队列(Work Queue),它会发送一些耗时的任务给多个工作者(Worker)。
工作队列(又称:任务队列——Task Queues)是为了避免等待一些占用大量资源、时间的操作。当我们把任务(Task)当作消息发送到队列中,一个运行在后台的工作者(worker)进程就会取出任务然后处理。当你运行多个工作者(workers),任务就会在它们之间共享。(说白了就是一个生产者,一个队列,多个消费者。相对于上一个,多了一些规则策略的,如:公平分发策略,轮询分发策略等等)
这个概念在网络应用中是非常有用的,它可以在短暂的 HTTP 请求中处理一些复杂的任务。
案例:
发送消息NewTask.java
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 TASK_QUEUE_NAME = "task_queue";
/**
* 发送消息列表
*/
public static final String[] MSGS = {"task 1", "task 2", "task 3", "task 4", "task 5", "task 6"};
public static void main(String[] argv) throws Exception
{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("120.79.XXX.XXX");
try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel())
{
// 持久化消息
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
for(int i = 0; i < MSGS.length; i++){
channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, MSGS[i].getBytes("UTF-8"));
System.out.println(" [x] Sent '" + MSGS[i] + "'");
}
}
}
}
接受消息Worker.java
import com.rabbitmq.client.*;
public class Worker
{
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception
{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("120.79.XXX.XXX");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages.");
//这告诉RabbitMQ一次不同时向一个worker发送一条消息(公平调遣)
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) ->
{
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try
{
// 让该消费者延迟2S,模拟任务较复杂情况
doWork();
} finally
{
System.out.println(" [x] Done");
/**
* basicAck 确认接受一条或者多条消息
* basicAck(long deliveryTag, boolean multiple)
*
* deliveryTag:该消息的index
* multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
/**
* basicConsume(String queue, boolean autoAck, DeliverCallback deliverCallback, CancelCallback cancelCallback)
* queue:队列名
* autoAck:是否自动确认消息,true自动确认,false 不自动要手动调用,建立设置为false
* deliverCallback 回调方法
* cancelCallback 使用者对象的接口
*/
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
private static void doWork()
{
try
{
Thread.sleep(2000);
} catch (InterruptedException e)
{
Thread.currentThread().interrupt();
}
}
}
启动一个NewTask,然后启动两个Worker
就会有一下结果:
NewTask:
Worker1:
Worker2:
从以上结果能看出,两个Worker把消息队列分解了。
消息确认
执行任务可能需要几秒钟。你可能想知道如果其中一个消费者开始一项长期任务并且只是部分完成而死亡会发生什么。使用我们当前的代码,一旦RabbitMQ向消费者发送消息,它立即将其标记为删除。在这种情况下,如果你杀死一个工人,我们将丢失它刚刚处理的消息。我们还将丢失分发给这个特定工作者但尚未处理的所有消息。
但我们不想失去任何任务。如果工人死亡,我们希望将任务交付给另一名工人。
为了确保消息永不丢失,RabbitMQ支持 消息确认。消费者发回ack(nowledgement)告诉RabbitMQ已收到,处理了特定消息,RabbitMQ可以自由删除它。
如果消费者死亡(其通道关闭,连接关闭或TCP连接丢失)而不发送确认,RabbitMQ将理解消息未完全处理并将重新排队。如果同时有其他在线消费者,则会迅速将其重新发送给其他消费者。这样你就可以确保没有消息丢失,即使工人偶尔会死亡。
没有任何消息超时; 当消费者死亡时,RabbitMQ将重新发送消息。即使处理消息需要非常长的时间,也没关系。
默认情况下,手动消息确认已打开。在前面的示例中,我们通过autoAck = true 标志明确地将它们关闭。一旦我们完成任务,就应该将此标志设置为false并从工作人员发送正确的确认。
Wroker1在执行到 ‘task 5’的时候我把程序强行停止
[*] Waiting for messages.
[x] Received 'task 3'
[x] Done
[x] Received 'task 5'
Wroker2就会执行task 6
[*] Waiting for messages.
[x] Received 'task 1'
[x] Done
[x] Received 'task 2'
[x] Done
[x] Received 'task 4'
[x] Done
[x] Received 'task 6'
[x] Done
PS:手速不够快,可以增加程序睡眠时间
消息持久性
我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是如果RabbitMQ服务器停止,我们的任务仍然会丢失。
当RabbitMQ退出或崩溃时,它将忘记队列和消息,除非你告诉它不要。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久。
首先,我们需要确保RabbitMQ永远不会丢失我们的队列。为此,我们需要声明它是持久的:
boolean durable = true ;
channel.queueDeclare(“hello”,durable,false,false,null);
此时我们可将RabbitMQ重新启动,task_queue队列也不会丢失。
公平派遣
您可能已经注意到调度仍然无法完全按照我们的意愿运行。例如,在有两个工人的情况下,当所有奇怪的消息都很重,甚至消息很轻时,一个工人将经常忙,而另一个工作人员几乎不会做任何工作。那么,RabbitMQ对此一无所知,仍然会均匀地发送消息。
发生这种情况是因为RabbitMQ只是在消息进入队列时调度消息。它不会查看消费者未确认消息的数量。它只是盲目地向第n个消费者发送每个第n个消息。
[外链图片转存失败(img-TY3NFU95-1568882407044)(https://www.rabbitmq.com/img/tutorials/prefetch-count.png)]
为了打败我们可以使用basicQos方法和 prefetchCount = 1设置。这告诉RabbitMQ一次不向一个worker发送一条消息。或者,换句话说,在处理并确认前一个消息之前,不要向工作人员发送新消息。相反,它会将它发送给下一个仍然很忙的工人。
int prefetchCount = 1 ;
channel.basicQos(prefetchCount);