RabbitMQ是一个message broker,本质上就是接收producer的消息,传递给consumer,其中可以根据给定的需要设置消息路由、缓存、持久化。
最简单的队列如下图,producer将消息推送到queue中,queue将消息传递给consumer。可以有多个producer向同一个queue推送消息,也可以有多个consumer从queue接收消息。
用Java开发producer和consumer需要下载Java client library。
producer代码如下:
// Send.java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class Send {
private final static String QUEUE_NAME = "SimpleQueue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
}
consumer代码如下:
// Recv.java
import com.rabbitmq.client.*;
import java.io.IOException;
public class Recv {
private final static String QUEUE_NAME = "SimpleQueue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
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(" [x] Received '" + message + "'");
}
};
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
编译并运行:
javac -cp rabbitmq-client.jar Send.java Recv.java
java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar Send
java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar Recv
运行结果如下:
任务队列是queue将消息分发给多个consumer进行处理。
// Task.java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class Task {
private final static String QUEUE_NAME = "TaskQueue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = argv[0];
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
}
// Worker.java
import com.rabbitmq.client.*;
import java.io.IOException;
public class Worker {
private final static String QUEUE_NAME = "TaskQueue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
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(" [x] Received '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] Done");
}
}
};
channel.basicConsume(QUEUE_NAME, true, consumer);
}
private static void doWork(String task) throws InterruptedException {
try {
Thread.sleep(1000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
编译代码:
javac -cp rabbitmq-client.jar Task.java Worker.java
分别打开三个console,其中两个运行Worker:
java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar Worker
另一个console运行Task发送消息:
java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar Task msg
运行结果如下:
使用任务队列可以实现并行任务处理。默认情况下,RabbitMQ会按消息的顺序依次分发给consumer,也就是说平均每个consumer会接收相同数量的消息。这种消息分发方式称为循环调度(round-robin dispatching)。
如果consumer在执行某个消息的任务过程中失败了,会怎么样呢?在当前模式下,RabbitMQ一旦将消息发送给了consumer就从内存中删除该消息,这种情况下,如果杀掉了worker,那么分配给该worker的未处理的和正在处理中的消息就会丢失。但是我们肯定不希望任务丢失,我们希望一个worker挂掉之后,它未处理完成的任务将分配给其他worker处理。
为了确保消息不会丢失,RabbitMQ支持消息反馈。消息反馈就是consumer告诉RabbitMQ某个消息已经被接收并处理完成了,可以从队列中删除了。
如果一个consumer挂掉了(channel或connection被关闭,或者TCP连接丢失),没有发送反馈(ack),那么RabbitMQ就知道消息没有被处理完成,就把它重新放入队列。这时如果有其他consumer在线,那么就把这个消息重新分发。这样就确保在worker挂掉时消息也不会丢失了。
消息处理是没有超时机制的。RabbitMQ会在consumer挂掉时才重新分发消息。因此,即使消息处理时间很长很长都没有关系。
默认消息反馈是开启的。在上面的例子中通过指定autoAck=true
关闭了反馈。消息反馈的代码如下:
final 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(" [x] Received '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] Done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
PS:可以使用rabbitmqctl查看未反馈的消息。
rabbitmqctl list_queues name messages_ready messages_unacknowledged
之前的消息反馈可以确保consumer挂掉时消息不会丢失,但是如果RabbitMQ服务器停止或者崩溃了,队列和其中的消息就都丢失了。要确保服务器上的消息不丢失,需要将队列和消息都标识成持久化的。
首先,确保RabbitMQ不会丢失队列:
boolean durable = true;
channel.queueDeclare("TaskQueue", durable, false, false, null);
尽管这代码是正确的,但是目前的环境下却不起作用。因为之前已经声明了一个非持久化的队列TaskQueue。RabbitMQ不允许用不同的参数重新定义已经存在的队列。换一个新的队列名称就行了。
boolean durable = true;
channel.queueDeclare("DurableTaskQueue", durable, false, false, null);
这样RabbitMQ重启后队列DurableTaskQueue是仍然存在的。接下来需要将消息标识成持久化的,通过设置属性MessageProperties为PERSISTENT_TEXT_PLAIN:
import com.rabbitmq.client.MessageProperties;
channel.basicPublish("", "task_queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
PS:将消息标识成持久化不能完全保证消息不会丢失。RabbitMQ在接收到消息写入磁盘之间仍有很短的时间间隔,而且RabbitMQ不会每次保存消息时都调用fsync,因此可能消息只被写入了缓存而还没有写入磁盘。如果需要更强的持久化方案,可以使用publisher confirm。
默认的循环调度方案可能会造成负载不平均。例如,有两个worker,偶数号的任务处理很耗时,而奇数号的任务处理很快,那么就会导致一个worker处理很慢,积压很多任务,而另一个worker处理很快,比较空闲。这是因为RabbitMQ在消息进入队列后就立刻分发出去,它不关心consumer中未反馈的消息数量,只是简单地将第n个消息分发给第n个consumer。
为了让每个consumer的负载均匀,通过basicQos方法设置参数prefetchCount为1。这样,consumer中的消息将不超过一条,当consumer处理完并发送反馈后RabbitMQ才会向它分发新的消息。RabbitMQ在接收到新消息后,如果有空闲的consumer就将消息发给它,否则就一直等待有空闲的consumer再分发。
int prefetchCount = 1;
channel.basicQos(prefetchCount);
PS:如果所有的任务都很耗时,worker都很忙,那么可能会导致队列被塞满,这时需要增加worker,或者采用其他策略。
最后的完整代码如下:
// Task.java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
public class Task {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
String message = argv[0];
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
// Worker.java
import com.rabbitmq.client.*;
import java.io.IOException;
public class Worker {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
int prefetchCount = 1;
channel.basicQos(prefetchCount);
final 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(" [x] Received '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] Done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
}
private static void doWork(String task) {
try {
Thread.sleep(1000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
参考资料:
RabbitMQ Introduction
RabbtMQ Work Queues