在上篇文章中使用队列解决了消息生产者(Producer)与消息消费者(Consumer)之间的异步通信问题。本文中,我们将创建一个工作队列,用于在多个Consumer之间分配资源密集型任务。工作队列的主要思想是避免立即执行并必须等待资源密集型任务完成,而是安排稍后完成任务。我们把一个任务封装成一个消息并发送给一个队列,这些任务将由Consumer完成。当你运行许多Consumer时,任务将它们间共享。这种思想在web应用程序中特别有用,在短的HTTP请求窗口中不可能处理复杂的任务。
准备
在上篇文章中,我们发送了一个包含“Hello World!”的消息。现在我们将发送代表复杂任务的字符串。我们通过使用Thread.sleep()函数来伪装执行资源密集型任务花费的时间。我们稍微修改上篇文章例子中的Producer.java代码。
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
private final static String QUEUE_NAME = "testQueue";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建一个到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("yst");
factory.setPassword("yst");
factory.setHost("192.168.17.64");
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (int i = 0; i < 5; i++) {
// 发送的消息
String message = "Hello World."+i;
// 往队列中发出一条消息
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("Sent:" + message + "'");
}
// 关闭渠道和连接;
channel.close();
conn.close();
}
}
稍微修改前面例子中的Producer.java代码。
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
private final static String QUEUE_NAME = "testQueue";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建一个到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("yst");
factory.setPassword("yst");
factory.setHost("192.168.17.64");
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
// 声明队列,主要为了防止消息接收者先运行此程序,队列还不存在时创建队列。
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println("Waiting for messages.");
// 创建队列消费者
final DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println("Received:" + message + "'");
try {
for (char ch : message.toCharArray()) {
if (ch == '.')
Thread.sleep(1000);
}
} catch (InterruptedException e) {
} finally {
System.out.println("Done!");
}
}
};
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
循环分发
使用任务队列的优点之一是适合扩展。如果负载过重,增加更多的Consumer来处理任务即可。
首先,我们尝试同时运行两个Consumer实例,我们称它们为C1,C2。
C1:
Waiting for messages.
C2:
Waiting for messages.
然后运行Producer发送消息,这时C1,C2中打印的内容为
C1:
Waiting for messages.
Received:Hello World.0'
Done!
Received:Hello World.2'
Done!
Received:Hello World.4'
Done!
C2:
Waiting for messages.
Received:Hello World.1'
Done!
Received:Hello World.3'
Done!
默认情况下,RabbitMQ将按顺序将每条消息发送给下一个Consumer。平均而言,每个消费者将获得相同数量的消息。这种分发消息的方式称为循环法(round-robin)。
公平派遣
您可能已经注意到调度仍然不能完全按照我们的要求工作。例如有两个工人,当所有奇数的消息都很复杂,而偶数的消息很轻松,一个工作人员就会一直很忙,而另一个工人几乎不会做任何工作。那么,如何处理这种问题呢?
我们可以使用basicQos方法告诉RabbitMQ一次不能给一个Consumer多个消息。或者说,在确认了前一个消息已经处理完之前,不要向Consumer发送新消息。
在Producer.java和Consumer.java中加入以下代码
int prefetchCount = 1 ;
channel.basicQos(prefetchCount);
待测试
消息确认
每个Consumer可能需要一段时间才能处理完收到的Message。在上面的代码中,channel.basicConsume(QUEUE_NAME, true, consumer);
中的第二个参数为true,代表no-ack。意为如果在处理Message的过程中,Consumer异常退出了,而Message还没有处理完成,那么这个Message就丢失了。因为我们采用no-ack的方式进行确认,也就是说,每次Consumer接到Message后,而不管是否处理完成,RabbitMQ Server会立即把这个Message标记为完成,然后从queue中删除了。
为了避免上述的情况,RabbitMQ提供了消息确认机制。为了保证Message不会因上述情况而丢失,不能采用no-ack。而应该是在处理完Message后发送ack,即告诉RabbitMQ,Message已经处理完,RabbitMQ可以去安全的删除它了。如果Consumer退出了但是没有发送ack,那么RabbitMQ就会把这个Message发送到下一个Consumer。这样就保证了在Consumer异常退出的情况下数据也不会丢失。
消息确认默认是打开的。
将上面代码中的channel.basicConsume(QUEUE_NAME, true, consumer);
第二个参数改为false,并在System.out.println("Done!");
后添加channel.basicAck(envelope.getDeliveryTag(),false);
进行测试。
C1
Waiting for messages.
Received:Hello World!0'
Done!
Received:Hello World!2....'
C2
Waiting for messages.
Received:Hello World!1..'
Done!
Received:Hello World!2....'
Done!
Received:Hello World!3......'
Done!
Received:Hello World!4........'
Done!
我在C1处理消息Hello World!2....
时中断了它,但可以看出C2继续执行了它。
持久化队列与消息
上面已经学习了如何确保即使Consumer死亡,Message也不会丢失。但是如果RabbitMQ服务器停止,Message仍然会丢失。
为了确保Message即使在RabbitMQ停止时也不会丢失,我们需要将queue和Message持久化。
持久化queue
首先,我们需要确保RabbitMQ永远不会失去队列。为了做到这一点,我们需要宣布它是持久的:
boolean durable = true ;
channel.queueDeclare(QUEUE_NAME, durable , false, false, null);
值得注意的是,这个命令不能修改已有的队列。虽然这个命令本身是正确的,但是在我们目前的设置中不起作用。这是因为我们已经定义了一个名为testQueue的队列 ,这个队列并不耐用。RabbitMQ不允许你使用不同的参数重新定义一个已经存在的队列,并且会向任何尝试这样做的程序返回一个错误。但有一个快速的解决方法 - 让我们声明一个不同名称的队列,例如durableQueue。
boolean durable = true ;
channel.queueDeclare("durableQueue", durable , false, false, null);
将此更改应用于生产者和消费者代码。此时我们确信,即使RabbitMQ重新启动,task_queue队列也不会丢失。
持久化Message
现在我们需要将消息标记为持久的。通过将MessageProperties(实现BasicProperties)设置为值PERSISTENT_TEXT_PLAIN可实现消息的持久化。
将生产者代码channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
改为MessageProperties.PERSISTENT_TEXT_PLAIN
。
注意事项:将Message标记为永久并不能完全保证Message不会丢失。虽然它告诉RabbitMQ将Message保存到磁盘,但是当RabbitMQ接收到消息并且还没有保存消息时,仍然有一个很短的时间窗口。此外,RabbitMQ不会为每个消息执行fsync(2) - 它可能只是保存到缓存中,而不是写入磁盘。持久性保证不强,但对我们简单的任务队列来说已经足够了。如果您需要更强大的保证,那么您可以使用发布商确认。
最终版本
Producer.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 Producer {
private final static String QUEUE_NAME = "testQueue";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建一个到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("yst");
factory.setPassword("yst");
factory.setHost("192.168.17.64");
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
//持久化queue
boolean durable = true ;
channel.queueDeclare(QUEUE_NAME, durable , false, false, null);
//限制在确认了前一个消息已经处理完之前,不要向Consumer发送新消息。
int prefetchCount = 1;
channel.basicQos(prefetchCount);
for (int i = 0; i < 5; i++) {
// 发送的消息
String message = "Hello World."+i;
for(int j = 0 ; j <i; j ++){
message = message+".";
}
// 往队列中发出一条消息,并持久化消息
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
System.out.println("Sent:" + message + "'");
}
// 关闭渠道和连接;
channel.close();
conn.close();
}
}
Consumer.java
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
private final static String QUEUE_NAME = "testQueue";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建一个到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("yst");
factory.setPassword("yst");
factory.setHost("192.168.17.64");
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
//持久化queue
boolean durable = true ;
channel.queueDeclare(QUEUE_NAME, durable , false, false, null);
int prefetchCount = 1;
channel.basicQos(prefetchCount);
System.out.println("Waiting for messages.");
// 创建队列消费者
final DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println("Received:" + message + "'");
try {
for (char ch : message.toCharArray()) {
if (ch == '.')
Thread.sleep(1000);
}
} catch (InterruptedException e) {
} finally {
System.out.println("Done!");
}
}
};
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
参考资料:http://www.rabbitmq.com/tutorials/tutorial-two-java.html