前面介绍了队列接收和发送消息,这篇将学习如何创建一个工作队列来处理在多个消费者之间分配耗时的任务。工作队列(work queue),又称任务队列(task queue)。
工作队列的目的是为了避免立刻执行资源密集型任务、减少等待时间。将消息发送到队列,工作进程在后台从队列取出任务并处理。
准备
通过Thread.sleep()来模拟耗时的任务,通过在消息的末尾添加"."来表示处理时间,例如,Hello...
表示耗时3秒。
发送端
NewTask.java:
package com.xxyh.rabbitmq;
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 NewTask {
private static final String WORK_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(WORK_NAME, durable, false, false, null);
// 发送10条记录,每次在后面添加"."
for (int i = 0; i < 10; i++) {
String dot = "";
for (int j = 0; j <= i; j++) {
dot += ".";
}
String message = "work queue " + dot + dot.length();
channel.basicPublish("", WORK_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("utf-8"));
System.out.println(Thread.currentThread().getName() + "发送消息:" + message);
}
channel.close();
connection.close();
}
}
接收端
Work.java:
package com.xxyh.rabbitmq;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Work {
private static final String WORK_QUEUE = "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(WORK_QUEUE, durable, false, false, null);
// 设置同一个消费者在同一时间只能消费一条消息
channel.basicQos(1);
Consumer 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(Thread.currentThread().getName() + "接收消息:" + message);
try {
doWork(message);
System.out.println("消息接收完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 确认消息已经收到
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 取消autoAck
boolean autoAck = false;
channel.basicConsume(WORK_QUEUE, autoAck, consumer);
}
private static void doWork(String task) throws InterruptedException {
for (char ch : task.toCharArray()) {
if (ch == '.') {
Thread.sleep(1000);
}
}
}
}
Round-robin分发
使用任务队列的好处就是容易处理并发工作。如果积累了大量的工作,只需要增加工作者就可以了。对于上面的示例代码,可以启动两个Work,然后启动NewTask,执行结果如下:
Work1:
pool-1-thread-4接收消息:work queue .1
消息接收完毕
pool-1-thread-5接收消息:work queue ...3
消息接收完毕
pool-1-thread-6接收消息:work queue .....5
消息接收完毕
pool-1-thread-7接收消息:work queue .......7
消息接收完毕
pool-1-thread-8接收消息:work queue .........9
消息接收完毕
Work2:
pool-1-thread-4接收消息:work queue ..2
消息接收完毕
pool-1-thread-5接收消息:work queue ....4
消息接收完毕
pool-1-thread-6接收消息:work queue ......6
消息接收完毕
pool-1-thread-7接收消息:work queue ........8
消息接收完毕
pool-1-thread-8接收消息:work queue ..........10
消息接收完毕
NewTask:
main发送消息:work queue .1
main发送消息:work queue ..2
main发送消息:work queue ...3
main发送消息:work queue ....4
main发送消息:work queue .....5
main发送消息:work queue ......6
main发送消息:work queue .......7
main发送消息:work queue ........8
main发送消息:work queue .........9
main发送消息:work queue ..........10
默认情况下,RabbitMQ会依次发送消息到下一个队列。一般情况下,每个消费者会收到数量大致相同的消息。这种分发消息的方法称为round-robin。
消息确认(Message acknowledgment)
处理一个任务可能需要几秒钟的时间。如果一个消费者在执行过程中中断可能导致消息的丢失,显然这并不是我们希望的,RabbitMQ提供了消息确认机制,让消费者反馈已经收到消息的信息,然后RabbitMQ就可以自由产出这条消息了。
如果消费者中断了,没有发送确认消息,RabbitMQ会认为消息发送失败并将消息重新加入队列。如果同时有其他消费者在工作,它会马上重新发送该消息到另一个消费者。这种方式可以保证消息不丢失。
消息确认机制默认是开启的:
// 打开确认
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, consumer);
消息持久化(Message durability)
上面的方式可以保证某个消费者中断时,消息不会丢失。然而,如果RabbitmqMQ中断或崩溃,它是不会记住队列和消息的。为了实现消息不丢失,需要将队列和消息持久化。首先将队列持久化:
boolean durable = true;
channel.queueDeclare("hello", durable, false, false, null);
如果一个已经存在的队列没有被设置持久化(durable=false)的,是不能更改它的参数的。另外,必须在消息的发送端、可接收端同时修改队列的声明。
上面的设置保证了即使RabbitMQ重启,队列也不丢失。但是并不能保证消息一定能发送到接收端,还需要将消息持久化:
channel.basicPublish("", QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("utf-8"));
公平分发
在某些情况下,分发的效果并不让人满意。例如,在两个消费者的情况下,奇数任务多而偶数任务少,可能一个消费队列一直处于繁忙状态,而另一个几乎处于闲置状态。RabbitMQ并不知晓这一情况,它只负责分发队列里的消息,不考虑消息未确认的情况。它只是盲目地将第n个消息发送给第n个消费者。
channel.basicQos(1);
这行代码的作用是告诉RabbitMQ,不要在同一时间给同一个消费者超过1条消息。