RabbitMQ的工作队列
在上一个教程中,我们编写了程序来发送和接收来自命名队列的消息。在这一个中,我们将创建一个工作队列,用于在多个工作人员之间分配耗时的任务。
本次主要介绍的是队列的消息应答、消息公平派遣、消息持久性。
1 消息应答
在我们从队列中取出消息执行任务的时候,可能需要几秒,我们可能会有疑问,如果在我们还在处理这个消息的时候,我们是如何处理的?或者是当消费者取得队列中的消息,队列就把这个消息给删除了,但是如果消费者在取完消息执行任务的时候失败了,那么这个消息也就丢失了;但是实际我们是希望如果如果执行任务的消费者失败了,另外一个消费者还可以继续获得这个消息去执行任务。为了确保消息不被丢失,RabbitMQ支持消息应答。当消费者获得消息任务成功执行后,告诉RabbitMQ,这个消息没用了,可以删除了;RabbitMQ收到消费者应答后,再删除队列中的消息。
如果消费者死机(其通道关闭,连接关闭或TCP连接丢失),而不发送确认信息(不应答),RabbitMQ将会明白消息未被完全处理并重新排队。如果同时有其他消费者在线,则会迅速将其重新提供给另一个消费者。这样就可以确保没有消息丢失,即使工作人员偶尔也会死亡。
消息应答主要是消费者服务中设置,在消费者指定消费队列的时候,打开手动应答机制
// 消费者指定消费队列,打开应答机制,注意false才是打开手动应对,true为自动应答
boolean ack =false;
channel.basicConsume(QUEUE_NAME, ack, consumer);
1.1 消费者完整代码如下
package MQ.WorkQueues.Response;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
@SuppressWarnings("deprecation")
/**
* @Title: MQ.WorkQueues.Worker.java
* @Package MQ.WorkQueues
* @Description:TODO(模拟MQ接收队列消息后手动应答,持久性)
* @Copyright: Copyright (c) 2017 YUANH All Rights Reserved
* @authoryuanh
* @date 2017-5-10下午3:48:59
*/
public classWorkerResponse {
private final static String QUEUE_NAME ="hello";
public static void main(String[] argv)throws Exception {
// 创建连接连接到MabbitMQ
ConnectionFactoryfactory = newConnectionFactory();
// 设置MabbitMQ所在主机ip或者主机名
factory.setHost("127.0.0.1");
factory.setUsername("yuanh");
factory.setPassword("yuanh");
factory.setPort(5672);
factory.setVirtualHost("y_yuanh");
Connectionconnection = factory.newConnection();
Channelchannel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false, false,false, null);
System.out.println(" [*]接收消息等待中,推出请按 CTRL+C");
QueueingConsumerconsumer = newQueueingConsumer(channel);
// 消费者指定消费队列,打开应答机制,注意false才是打开手动应对,true为自动应答
boolean ack =false;
channel.basicConsume(QUEUE_NAME, ack, consumer);
try {
while (true) {
QueueingConsumer.Deliverydelivery = consumer.nextDelivery();
Stringmessage = newString(delivery.getBody());
System.out.println("接收 '" + message +"'");
try {
doWork(message);
}finally{
System.out.println("结束");
// 另外需要在每次处理完成一个消息后,手动发送一次应答(ack=false)。
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),
false);
}
}
}catch(Exception e) {
channel.close();
connection.close();
}
}
private static void doWork(String task)throws InterruptedException {
for (char ch :task.toCharArray()) {
if (ch =='.')
Thread.sleep(1000);
}
}
}
1.2 生产者完整代码如下
package MQ.WorkQueues.Response;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* @Title: MQ.WorkQueues.NewTask.java
* @Package MQ.WorkQueues
* @Description:TODO(MQ往队列中放消息)
* @Copyright: Copyright (c) 2017 YUANH All Rights Reserved
* @authoryuanh
* @date 2017-5-10下午3:50:35
*/
public classNewTask {
private final static String QUEUE_NAME ="hello";
public static void main(String[] args)throws IOException,TimeoutException {
// 创建连接连接到MabbitMQ
ConnectionFactoryfactory = newConnectionFactory();
// 设置MabbitMQ所在主机ip或者主机名
factory.setHost("127.0.0.1");
factory.setUsername("yuanh");
factory.setPassword("yuanh");
factory.setPort(5672);
factory.setVirtualHost("y_yuanh");
Connectionconnection = factory.newConnection();
// 创建一个频道
Channelchannel = connection.createChannel();
// 创建一个队列
channel.queueDeclare(QUEUE_NAME,false, false,false, null);
Stringmessage = getMessage(args);
// 将消息放到队列里面
channel.basicPublish("",QUEUE_NAME, null, message.getBytes());
System.out.println("发送 '" + message +"'");
// 关闭通道和连接
channel.close();
connection.close();
}
private static StringgetMessage(String[] strings){
if (strings.length < 1)
return"Hello World..!";
returnjoinStrings(strings, " ");
}
private static StringjoinStrings(String[] strings, String delimiter) {
int length = strings.length;
if (length == 0)return "";
StringBuilder words = newStringBuilder(strings[0]);
for (int i = 1; i < length; i++) {
words.append(delimiter).append(strings[i]);
}
return words.toString();
}
}
2 消息公平派遣
例如在两个工人的情况下,当所有信息都很重,甚至信息很轻的时候,一个工作人员将不断忙碌,另一个工作人员几乎不会做任何工作。然而,RabbitMQ不知道什么,只知道随机分配。它只是盲目地向第n个消费者发送每个第n个消息。这是因为当消息进入队列时,RabbitMQ只会分派消息。它不看消费者的未处理完的消息数量。
为了打破这个机制,我们可以使用basicQos方法与 prefetchCount = 1设置。这告诉RabbitMQ不要一次给一个工作者多个消息。或者换句话说,在处理并确认前一个消息之前,不要向工作人员发送新消息。相反,它将发送到下一个还不忙的消费者。
在消费者中添加如下代码
// 消费者指定消费队列,打开应答机制,注意false才是打开手动应对,true为自动应答
boolean ack =false;
channel.basicConsume(QUEUE_NAME, ack, consumer);
// 消费者设置最大服务转发消息数量,公平转发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
2.1 消费者完整代码如下(可设置多个消费者模拟)
package MQ.WorkQueues.FairForward;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
@SuppressWarnings("deprecation")
/**
* @Title: MQ.WorkQueues.Worker.java
* @Package MQ.WorkQueues
* @Description:TODO(模拟MQ公平转发队列消息)
* @Copyright: Copyright (c) 2017 YUANH All Rights Reserved
* @authoryuanh
* @date 2017-5-10下午3:48:59
*/
public classWorkerResponse {
private final static String QUEUE_NAME ="hello";
public static void main(String[] argv)throws Exception {
// 创建连接连接到MabbitMQ
ConnectionFactoryfactory = newConnectionFactory();
// 设置MabbitMQ所在主机ip或者主机名
factory.setHost("127.0.0.1");
factory.setUsername("yuanh");
factory.setPassword("yuanh");
factory.setPort(5672);
factory.setVirtualHost("y_yuanh");
Connectionconnection = factory.newConnection();
Channelchannel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false, false,false, null);
System.out.println(" [*]接收消息等待中,推出请按 CTRL+C");
QueueingConsumerconsumer = newQueueingConsumer(channel);
// 消费者指定消费队列,打开应答机制,注意false才是打开手动应对,true为自动应答
boolean ack =false;
channel.basicConsume(QUEUE_NAME, ack, consumer);
// 消费者设置最大服务转发消息数量,公平转发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
try {
while (true) {
QueueingConsumer.Deliverydelivery = consumer.nextDelivery();
Stringmessage = newString(delivery.getBody());
System.out.println("接收 '" + message +"'");
try {
doWork(message);
}finally{
System.out.println("结束");
// 另外需要在每次处理完成一个消息后,手动发送一次应答(ack=false)。
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),
false);
}
}
}catch(Exception e) {
channel.close();
connection.close();
}
}
private static void doWork(String task)throws InterruptedException {
for (char ch :task.toCharArray()) {
if (ch =='.')
Thread.sleep(1000);
}
}
}
2.2 生产者完整代码如消息应答
3 消息持久性
我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是如果RabbitMQ服务器停止,我们的任务仍然会丢失。当RabbitMQ退出或崩溃时,它会忘记队列和消息,除非你不告诉它。需要两件事来确保消息不会丢失:我们需要将队列和消息标记为耐用。
首先,我们需要确保RabbitMQ不会失去我们的队列。为了这样做,我们需要将其声明为持久的。
生产者、消费者都应在在声明队列的时候设置持久化,添加代码如下
// 声明队列、设置队列持久化
boolean durable =true;
channel.queueDeclare(QUEUE_NAME, durable,false, false,null);
3.1 消费者完整代码如下
package MQ.WorkQueues.Persistent;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
@SuppressWarnings("deprecation")
/**
* @Title: MQ.WorkQueues.Worker.java
* @Package MQ.WorkQueues
* @Description:TODO(模拟MQ在停止服务后队列信息还是存在,持久性)
* @Copyright: Copyright (c) 2017 YUANH All Rights Reserved
* @authoryuanh
* @date 2017-5-10下午3:48:59
*/
public classWorkerResponse {
private final static String QUEUE_NAME ="hello";
public static void main(String[] argv)throws Exception {
// 创建连接连接到MabbitMQ
ConnectionFactoryfactory = newConnectionFactory();
// 设置MabbitMQ所在主机ip或者主机名
factory.setHost("127.0.0.1");
factory.setUsername("yuanh");
factory.setPassword("yuanh");
factory.setPort(5672);
factory.setVirtualHost("y_yuanh");
Connectionconnection = factory.newConnection();
Channelchannel = connection.createChannel();
// 1声明队列、设置队列持久化
boolean durable =true;
channel.queueDeclare(QUEUE_NAME, durable,false, false,null);
System.out.println(" [*]接收消息等待中,推出请按 CTRL+C");
QueueingConsumerconsumer = newQueueingConsumer(channel);
// 2消费者指定消费队列,打开应答机制,注意false才是打开手动应对,true为自动应答
boolean ack =false;
channel.basicConsume(QUEUE_NAME, ack, consumer);
// 3消费者设置最大服务转发消息数量,公平转发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
try {
while (true) {
QueueingConsumer.Deliverydelivery = consumer.nextDelivery();
Stringmessage = newString(delivery.getBody());
System.out.println("接收 '" + message +"'");
try {
doWork(message);
}finally{
System.out.println("结束");
// 另外需要在每次处理完成一个消息后,手动发送一次应答(ack=false)。
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),
false);
}
}
}catch(Exception e) {
channel.close();
connection.close();
}
}
private static void doWork(String task)throws InterruptedException {
for (char ch :task.toCharArray()) {
if (ch =='.')
Thread.sleep(1000);
}
}
}
3.2 生产者完整代码如下
package MQ.WorkQueues.Persistent;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* @Title: MQ.WorkQueues.NewTask.java
* @Package MQ.WorkQueues
* @Description:TODO(模拟MQ在停止服务后队列信息还是存在)
* @Copyright: Copyright (c) 2017 YUANH All Rights Reserved
* @authoryuanh
* @date 2017-5-10下午3:50:35
*/
public classNewTask {
private final static String QUEUE_NAME ="hello";
public static void main(String[] args)throws IOException,TimeoutException {
// 创建连接连接到MabbitMQ
ConnectionFactoryfactory = newConnectionFactory();
// 设置MabbitMQ所在主机ip或者主机名
factory.setHost("127.0.0.1");
factory.setUsername("yuanh");
factory.setPassword("yuanh");
factory.setPort(5672);
factory.setVirtualHost("y_yuanh");
Connectionconnection = factory.newConnection();
// 创建一个频道
Channelchannel = connection.createChannel();
// 声明队列、设置队列持久化
boolean durable =true;
channel.queueDeclare(QUEUE_NAME, durable,false, false,null);
Stringmessage = getMessage(args);
// 将消息放到队列里面
channel.basicPublish("",QUEUE_NAME, null, message.getBytes());
System.out.println("发送 '" + message +"'");
// 关闭通道和连接
channel.close();
connection.close();
}
private static StringgetMessage(String[] strings){
if (strings.length < 1)
return"Hello World..!";
returnjoinStrings(strings, " ");
}
private static StringjoinStrings(String[] strings, String delimiter) {
int length = strings.length;
if (length == 0)return "";
StringBuilder words = newStringBuilder(strings[0]);
for (int i = 1; i < length; i++) {
words.append(delimiter).append(strings[i]);
}
return words.toString();
}
}