一、概述
The main idea behind Work Queues (aka: Task Queues) is to avoid doing a resource-intensive task immediately and having to wait for it to complete.Instead we schedule the task to be done later. 工作队列(即任务队列)背后的主要思想是避免立即执行资源密集型任务,并且必须等待它完成。相反,我们将工作安排在稍后执行。
工作队列的概念对WEB应用尤其有用,因为往往很难在短时间的HTTP请求窗口中完成复杂的任务。工作队列的优点之一是更轻松地处理并行工作。如果我们的工作快速积压,那么可以增加更多的工人,这样,规模就很容易扩大。当队列中有很多耗时任务时,增加多个消费者可以分摊这些任务,从而提高并发性能。
二、循环分配(Round-robin dispatching)
2.1 概念
默认情况下,RabbitMQ将按顺序将每个消息发送给下一个使用者。
2.2 实例
实例源码参考官方教程:
https://www.rabbitmq.com/tutorials/tutorial-two-java.html
运行测试结果如下:
Worker1:
[*] Waiting for messages. To exit press CTRL+C
[x] Received 'First message.'
[x] Done
[x] Received 'Third message...'
[x] Done
Worker2:
[*] Waiting for messages. To exit press CTRL+C
[x] Received ''
[x] Done
[x] Received 'Second message..'
[x] Done
三、消息确认(message acknowledgment)
做任务需要花费时间,如果其中一个消费者启动了一个很长的任务,但只完成了一部分就挂掉了,会发生什么情况?
在现阶段的代码中,一旦worker接收到了消息,rabbitmq会立即标记该消息为已处理。如果此时worker挂掉了,那么该消息就将会丢失。
这种情形显然是不能别接受的,我们希望的是,如果一个worker挂掉了,那么该任务会被投递到其他worker继续完成。
为了避免消息丢失,RabbitMQ提供消息确认机制,当消息被接受、处理后,消费者会给RabbitMQ发送一个ACK,让RabbitMQ可以安心删除该消息。如果消费者挂掉了,没有返回ACK,那么RabbitMQ就会将该任务重新装入队列并分配给其他空闲的消费者。这样,即使有消费者偶尔挂掉了,也可以确保没有任何消息会丢失。
同时,由于没有任何消息超时时间,所以即使一项任务需要花费很长很长的时间也没有关系。
默认情况下,将打开手动消息确认。在前面的示例中,我们通过autoAck=true标志显式地关闭了它们。我们可以将该标志设置为false,并从worker发送一个适当的确认。
DeliverCallback deliverCallback = (consumerTag,delivery)->{
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(" [x] Done");
//2.手动确认,如果该进程中途挂掉,那么消息会被重新投递到另一个消费者
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
boolean autoAck = false; // 1.关闭自动确认功能
channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
确认必须和接受消息的消费者使用同一个channel,否则会产生一个channel级别的protocol 异常。
漏掉ACK是一个常见的错误,但它的后果很严重。当该消费者退出时,所有曾发送给它的消息都将会被再次投递!!!并且RabbitMQ将消耗越来越多的内存,因为它将无法释放任何未加密的消息。
四、持久性durable
使用ack机制可以确保消费者挂掉时不会丢失消息,那么当RabbitMQ Server挂掉的时候呢,又该怎样避免消息丢失呢?
4.1 队列的持久性
当RabbitMQ Server退出或者挂掉时,它会丢掉所有的队列和消息,除非我们明确地给队列和消息打上durable的标记。示例如下:
boolean durable = true;
channel.queueDeclare("workQueue", durable, false, false, null);
注意,RabbitMQ不允许重新定义队列的持久化属性,也就是说durable参数只对新创建的队列有效。
当我们将durable属性设置为true时,即使RabbitMQ Server重启了,workQueue队列也不会丢失.
注意,queueDeclare在生产者和消费者中都要声明durable属性为true。
4.2 消息的持久性
队列通过durable属性进行持久化,那么消息呢?消息是通过将MessageProperties设置为PERSISTENT_TEXT_PLAIN实现的:
import com.rabbitmq.client.MessageProperties;
channel.basicPublish("", "task_queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
该标志告诉RabbitMQ将本条消息存储到磁盘中,以达到持久化的目的。但该措施并不能百分百保证持久化,因为在收到消息和存储到磁盘这个过程中有一个时间窗口,如果在该窗口中Server挂掉了,那么消息也会丢失。如果需要更严格的保证,那么可以使用publisher confirms。
五、公平分发Fair Dispatch
5.1 问题
到目前为止,如果有多个消费者,我们的分发机制是轮流分发。这种机制存在一些问题,例如所有排在奇数位置的都是重任务、在偶数的都是轻任务,这样就会导致其中一个Worker一直处于繁忙状态。
这是因为RabbitMQ只在消息进入队列时进行分发,它不管上一条消息是否已经处理完毕(收到ack),只是盲目地将第n条消息发送给地n个消费者。
5.2 解决方案——basicQos
我们可以通过basicQos方法和prefetchCount=1来告诉RabbitMQ,不要同时给一个发消费者发送多条消息,即只有在消费者已经处理完并回复ack后再为其分配下一条消息。这样,就能将消费者资源最大化利用,谁空闲,谁处理,实现相对公平的分发。
Worker.java(消费者)
int prefetchCount=1;
channel.basicQos(prefetchCount);
六、整体代码
NewTask.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 NewTask {
private final static String QUEUE_NAME="task_queue";
public static void main(String[] args) {
//1 实例化连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("admin");
factory.setPassword("admin");
//factory.setHost("www.deardull.com");
try {
//2 创建连接
Connection conn = factory.newConnection();
//3 创建channel.接下来,我们创建一个通道,用于完成任务的大部分API都位于这个通道中。
Channel channel = conn.createChannel();
//4 声明队列,发送的消息将送到该队列中
boolean durable=true; //队列持久化
channel.queueDeclare(QUEUE_NAME,durable,false,false,null);
//5 发送消息
String message = String.join(" ", args);
//MessageProperties.PERSISTENT_TEXT_PLAIN 消息持久化
channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
System.out.println("[send]"+message);
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
}
Worker.java(消费者)
public class Worker {
private final static String QUEUE_NAME = "task_queue";
public static void main(String[] args) throws IOException, TimeoutException {
//1 连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2 设置连接属性
factory.setHost("127.0.0.1");
factory.setUsername("admin");
factory.setPassword("admin");
//3 获取连接
Connection conn = factory.newConnection();
//4 获取channel
Channel channel = conn.createChannel();
//5 为通道绑定队列
boolean durable = true; //持久化属性
channel.queueDeclare(QUEUE_NAME,durable,false,false,null);
//6 每次只接收一条消息,处理完再接收下一条
channel.basicQos(1);
System.out.println("[*] Waiting for messages. To exit press CTRL+C");
//7 定义回调函数
DeliverCallback deliverCallback = (consumerTag,delivery)->{
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(" [x] Done");
//手动确认,如果该进程中途挂掉,那么消息会被重新投递到另一个消费者
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
//8 消费消息
boolean autoAck = false; // 关闭自动确认功能
channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
}
private static void doWork(String task) throws InterruptedException {
for (char ch: task.toCharArray()) {
if (ch == '.') Thread.sleep(1000);
}
}
}
七、参考文档
参考RabbitMQ官方文档:https://www.rabbitmq.com/tutorials/tutorial-two-java.html