在第一个教程中,我们写的程序发送和接收消息从命名队列中。在这其中,我们将创建一个工作队列将被用来分配定时消费任务通过多个工作者。
工作队列“(又名:任务队列)背后的主要想法是为了避免立即做一个资源密集型的任务,不必等待它完成。相反,我们安排在稍后进行的任务。我们封装 任务为消息发送到队列。工作进程在后台运行,会弹出任务和最终执行作业。当您运行许多工作者的任务都在它们之间共享。
这个概念在web应用中是特别有用的,这是不可能的,在很短的HTTP请求窗口处理一个复杂的任务。
在本教程的前面部分,我们发了包含的“Hello World!”的消息。 现在,我们将要发送的字符串代表复杂的任务。我们没有一个真实世界的任务,像图像大小或PDF文件进行渲染,所以让我们假装很忙通过使用Thread.sleep() 函数。我们将采取的字符串作为它的复杂性,每一个点的数量将占到二分之一“工作”( every dot will account for one second of "work")。例如,一个假设的任务通过描述hello.. 将花费三秒钟。
从我们前面的例子中,我们将稍微修改的Send.java代码,允许任意消息通过命令行发送。这一计划将安排到我们的工作队列中的任务,让我们将其命名为 NewTask.java:
String message = getMessage(argv);
channel.basicPublish("", "hello", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
从命令行参数中获得信息得到帮助:
privatestaticStringgetMessage(String[]strings){
if(strings.length<1)
return"Hello World!";
returnjoinStrings(strings," ");
}
privatestaticStringjoinStrings(String[]strings,Stringdelimiter){
intlength=strings.length;
if(length==0)return"";
StringBuilderwords=newStringBuilder(strings[0]);
for(inti=1;i<length;i++){
words.append(delimiter).append(strings[i]);
}
returnwords.toString();
}
我们的老Recv.java脚本也需要一些变化:它需要假设第二份工作是为消息体中的每一个点。它会推出队列中的消息,并执行任务,让我们叫它Worker.java:
while (true) {
QueueingConsumer.Deliverydelivery = consumer.nextDelivery();
Stringmessage = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
doWork(message);
System.out.println(" [x] Done");
}
我们的假任务以模拟执行时间:
private static voiddoWork(Stringtask) throws InterruptedException {
for (char ch: task.toCharArray()) {
if (ch == '.')Thread.sleep(1000);
}
}
编译他们在上一个教程中(在工作目录下的jar文件):
$ javac -cp rabbitmq-client.jar NewTask.java Worker.java
使用任务队列的优点之一是能够方便地并行工作。如果我们建立了积压的工作,我们就可以添加更多的工人,这样大规模应用容易。
首先,让我们的尝试同时运行两个Worker.java脚本。他们都将获得队列中的消息,但究竟如何?让我们来看看。
你需要打开三个控制台。两个将运行Worker.java的 脚本。这些控制台将是我们两个消费者- C1和C2。
shell1$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
Worker
[*] Waiting for messages. To exit press CTRL+C
shell2$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
Worker
[*] Waiting for messages. To exit press CTRL+C
在第三个控制台我们将发布一个新的任务。一旦你启动了消费者你将可以发布一些消息:
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask First message.
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Second message..
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Third message...
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Fourth message....
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Fifth message.....
让我们来看看我们给工作者传递的:
shell1$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
Worker
[*] Waiting for messages. To exit press CTRL+C
[x] Received 'First message.'
[x] Received 'Third message...'
[x] Received 'Fifth message.....'
java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
Worker
[*] Waiting for messages. To exit press CTRL+C
[x] Received 'Second message..'
[x] Received 'Fourth message....'
在默认的情况下 RabbitMQ将发送每个消息给下一个消费者,在序列里。平均而言,每名消费者将获得相同数量的消息。这种方式被称为分发消息循环赛。试试这个有三个或更多的工人。
执行一个任务需要花费几秒钟。你可能想知道会发生什么,如果一个消费者启动一项长期的任务,它只是部分完成死亡。我们当前的代码,一旦RabbitMQ的提供了一个消息给客户立即将其从内存中删除。在这种情况下,如果你杀了一个工作者,我们就失去了消息,它只是处理。我们也将失去所有的消息,分赴这个特殊的工人,但尚未办理。
但是我们不希望失去任何任务。如果一个工作者死亡,我们希望被传递到另一个工作者的任务。
为了确保每条消息都不丢失,RabbitMQ的支持消息的确认(acknowledgments)。一个确认(acknowledgments)被送回消费者告诉RabbitMQ的一个特定的消息已被接收和处理,RabbitMQ可以自由删除它(message)。
如果消费者死而没有发送一个确认,RabbitMQ会明白这个个消息是没有完全处理并重新传送到另一个消费者。这样可以确保消息不会丢失,即使工作者(the workers)偶尔死。
目前还没有任何消息超时,只有当工作者的连接中断,RabbitMQ将重新传送消息。即使重新处理消息需要一个很长很长的时间。
消息确认默认打开的。在前面的例子中,我们明确地把他们通过AUTOACK=true的 标志。是时候移除这个标志,并发送一个确认给工作者(the worker),一旦我们完成了任务。
QueueingConsumerconsumer = new QueueingConsumer(channel);
booleanautoAck = false;
channel.basicConsume("hello",autoAck,consumer);
while (true) {
QueueingConsumer.Deliverydelivery = consumer.nextDelivery();
//...
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
使用此代码,我们可以肯定的是,即使你使用CTRL + C杀死一个工作者,当它处理消息的时候,什么都不会丢失。过会这个工作者死亡,所有没有确认的消息都将重新发送。
这是一个常见的错误错过basicAck的。这是一个简单的错误,但后果是严重的。当您的客户端将退出(这可能看起来像随机重新发送)讯息会重新传送,但RabbitMQ的将吃越来越多的内存,因为它无法释放任何没确认的消息。
为了调试这种错误,你可以使用用rabbitmqctl的 打印messages_unacknowledged的领域,
$ sudo rabbitmqctl list_queues namemessages_ready messages_unacknowledged
Listing queues ...
hello 0 0
...done.
我们已经学会了如何确保即使消费者死了任务也不会丢失。但是,我们的任务仍然会被丢失当RabbitMQ的服务器停止时。
当RabbitMQ的退出或崩溃了,它会忘记的队列和消息,除非你告诉它不要。需要做两件事情以确保消息不会丢失:我们需要标记队列和消息耐用。
首先,我们需要确保RabbitMQ的永远不会失去我们的队列。为了做到这一点,我们需要将其声明为耐用:
booleandurable = true;
channel.queueDeclare("hello",durable, false, false, null);
虽然这个命令本身是正确的,也不会在我们目前的设置工作。这是因为我们已经定义了一个不持久的名为hello的队列 。RabbitMQ的不允许你用不同的参数重新定义现有的队列,当你试图做这一点将返回一个错误。但有一个快速解决方案-让我们的声明一个队列具有不同的名称,例如task_queue:
booleandurable = true;
channel.queueDeclare("task_queue",durable, false, false, null);
此queueDeclare变化需要被应用到兼顾生产者和消费者的代码。
在这一点上,我们肯定不会丢失,即使的task_queue队列RabbitMQ的重新启动。现在,我们需要我们的消息标记为持久性-通过设置MessageProperties(实现BasicProperties)价值PERSISTENT_TEXT_PLAIN的。
import com.rabbitmq.client.MessageProperties;
channel.basicPublish("","task_queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
标记为持久的消息并不能完全保证消息不会丢失。虽然它告诉RabbitMQ的消息保存到磁盘,仍然有一个很短的时间窗口时,RabbitMQ的消息,并已经接受了,还没有保存它。此外,RabbitMQ的不会做的fsync(2)每封邮件-可以保存到缓存,并没有真正写入到磁盘。持久性担保能力不强,但它是我们简单的任务队列绰绰有余。如果你需要一个更强有力的保证,你可以用出版的代码在一个事务中。
您可能已经注意到正如我们希望调度不能正常工作。例如,在某种情况下的两个工作者,所有奇数的消息是重量级的(heavy),偶数的消息是轻量级的,一名工作者将非常忙碌而另外一个工作者几乎不做任何工作。RabbitMQ仍然会派遣消息均匀。
这是因为RabbitMQ在当消息进入队列后仅仅只是分发消息。它没有为消费者查找未确认的消息。它只是一味地分发n个消息给n个消费。
为了打败我们可以使用basicQos的方法与 prefetchCount = 1设置。这告诉RabbitMQ的不给一次给一个以上的消息给工作者。或者,换句话说,分发一个新的消息给工作者直到它已处理和返回前一个消息的确认。相反,它会分发给下一个不忙的工作者。
intprefetchCount = 1;
channel.basicQos(prefetchCount);
如果所有的工人都在忙,你的队列可以填满。您将要留意,也许添加更多的工人,或有一些其他的策略。
最终代码我们的NewTask.java类:
import java.io.IOException;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
public class NewTask {
private static final StringTASK_QUEUE_NAME = "task_queue";
public static voidmain(String[]argv)
throws java.io.IOException {
ConnectionFactoryfactory = new ConnectionFactory();
factory.setHost("localhost");
Connectionconnection = factory.newConnection();
Channelchannel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
Stringmessage = getMessage(argv);
channel.basicPublish("",TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
//...
}
Andour Worker.java:
import java.io.IOException;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;
public class Worker {
private static final StringTASK_QUEUE_NAME = "task_queue";
public static voidmain(String[]argv)
throws java.io.IOException,
java.lang.InterruptedException {
ConnectionFactoryfactory = new ConnectionFactory();
factory.setHost("localhost");
Connectionconnection = factory.newConnection();
Channelchannel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
channel.basicQos(1);
QueueingConsumerconsumer = new QueueingConsumer(channel);
channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
while (true) {
QueueingConsumer.Deliverydelivery = consumer.nextDelivery();
Stringmessage = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
doWork(message);
System.out.println(" [x] Done" );
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
//...
}
使用消息的确认和prefetchCount你可以设立一个工作队列。耐久性属性让任务生存即使重新启动RabbitMQ的时候。
通道方法和MessageProperties欲了解更多信息,您可以浏览 网上的javadoc。
现在我们可以将教程3,学习如何传递同样的信息让不少消费者。