RabbitMQ
的consumer
可以在Pull
模式处理消息, 也可以在push
模式下处理消息.无论是pull模式还是在push模式, 都可以设置消息确认方式:自动确认(autoAck
为true),或者手动确认(autoAck
为false).autoAck
这个参数名存在可能存在一些歧义, 当这个参数为true时, RabbitMQ
服务器在发送消息到socket
插口后, 就会将消息移除,consumer
不会对消息进行确认.
关于消息确认方式需要注意如下细节:
RabbitMQ
服务器无法知道消息是否投递成功, consumer
是否成功的处理了投递的消息.自动确认方式的另一个影响是, 如果consumer
是push
模式, RabbitMQ服务器会持续的把消息投递到consumer上, 如果投递的消息数量比较大, consumer的处理能力比较慢, 这将对consumer的资源占用造成很大的压力.consumer
可以给RabbitMQ
服务器返回一个明确的确认结果, 而且consumer可以更灵活的掌握何时进行消息的确认. consumer可以在接收到消息时就确认(如果这种情况满足设计场景的话), 也可以在消息处理完毕,向服务器返回确认消息.RabbiteMQ服务器在没有收到消息确认时,将保持对该消息的引用.因此,这种方式是一种更可靠的处理方式.可以使用basicConsume
注册push
模式工作的consumer
, 当使用手动确认消息的方式时, 默认处理流程是:
在consumer
的实现中, 可以使用basicQos
API
改变这种默认的流程, 即,可以让RabbitMQ一次投递多个消息到consumer, 实现消息的预取(prefetch
). 下面通过一个完整的示例说明预取的操作.
示例代码
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.GetResponse;
import com.rabbitmq.client.AMQP.BasicProperties;
public class QSQueue {
private static final String NORMAL_EXCHANGE = "normal_exchange";
private static final String DEFAULT_WORKER_QUEUE = "queue_to_normal_exchange";
private static final String NORMAL_ROUTE_KEY = "normal_route_key";
private Connection connection;
// For consumer and producer schedule.
private final ExecutorService executorService;
private static class SimpleThreadFactory implements ThreadFactory {
private final AtomicInteger idx = new AtomicInteger(0);
public Thread newThread(Runnable r) {
return new Thread(r, "Thread#" + idx.getAndIncrement());
}
}
private static class WorkerConsumer implements Runnable {
private static final int PREFETCH_COUNTS = 2;
private final Connection connection;
private final boolean pushMode;
private Channel channel;
private final DeliverCallback deliverCallback = (consumerTag, delivery) -> {
final String message = new String(delivery.getBody(), "UTF-8");
final Envelope envelope = delivery.getEnvelope();
final long deliveryTag = envelope.getDeliveryTag();
final BasicProperties props = delivery.getProperties();
final String msgId = props.getMessageId();
System.out.println("Received msg:" + message
+ " delivery tag:" + deliveryTag
+ " msg id:" + msgId );
// Simulate a very time consuming work.
try {
Thread.sleep(1000*60*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
channel.basicAck(deliveryTag, false);
};
private void handleGettedMsg(GetResponse getResponse, boolean autoAck) throws IOException {
final String message = new String(getResponse.getBody(), "UTF-8");
final Envelope envelope = getResponse.getEnvelope();
final long deliveryTag = envelope.getDeliveryTag();
final BasicProperties props = getResponse.getProps();
final String msgId = props.getMessageId();
System.out.println("Getted msg:" + message
+ " delivery tag:" + deliveryTag
+ " msg id:" + msgId );
if (!autoAck) {
channel.basicAck(deliveryTag, false);
}
}
public WorkerConsumer(Connection connection, boolean pushMode) {
this.connection = connection;
this.pushMode = pushMode;
}
@Override
public void run() {
try {
channel = connection.createChannel();
if (pushMode) {
channel.basicQos(0, PREFETCH_COUNTS, false);
}
channel.exchangeDeclare(NORMAL_EXCHANGE, "direct");
channel.queueDeclare(DEFAULT_WORKER_QUEUE, false, false, true, null);
channel.queueBind(DEFAULT_WORKER_QUEUE, NORMAL_EXCHANGE, NORMAL_ROUTE_KEY);
// Push mode
if (pushMode) {
final String tag = channel.basicConsume(DEFAULT_WORKER_QUEUE, false, deliverCallback, consumerTag -> {System.out.println("Cancel consumer:" +consumerTag);});
System.out.println("worker consumer tag:" + tag);
// Register the second consumer on the channel.
/*final String tag2 = channel.basicConsume(DEFAULT_WORKER_QUEUE, false, deliverCallback, consumerTag -> {System.out.println("Cancel consumer:" +consumerTag);});
System.out.println("worker consumer#2 tag:" + tag2);*/
} else { // Pull mode
final boolean autoAck = false;
while (true) {
final GetResponse getResponse = channel.basicGet(DEFAULT_WORKER_QUEUE, autoAck);
handleGettedMsg(getResponse, autoAck);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static class Producer implements Runnable {
private static final int MSG_COUNTS = 10;
private final Connection connection;
public Producer(Connection connection) {
this.connection = connection;
}
@Override
public void run() {
try {
final Channel channel = connection.createChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE, "direct");
channel.queueDeclare(DEFAULT_WORKER_QUEUE, false, false, true, null);
channel.queueBind(DEFAULT_WORKER_QUEUE, NORMAL_EXCHANGE, NORMAL_ROUTE_KEY);
for (int i = 0; i < MSG_COUNTS; i++) {
final BasicProperties.Builder propsBuilder = new BasicProperties.Builder();
final BasicProperties props = propsBuilder.appId(Thread.currentThread().toString()).messageId(String.valueOf(i)).build();
channel.basicPublish(NORMAL_EXCHANGE, NORMAL_ROUTE_KEY, props, "[normal message.]".getBytes());
}
} catch(IOException e) {
e.printStackTrace();
}
}
}
public QSQueue() {
executorService = Executors.newCachedThreadPool(new SimpleThreadFactory());
final ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
try {
connection = connectionFactory.newConnection();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
public void execWorkderConsumer(boolean pushMode) {
executorService.submit(new WorkerConsumer(connection, pushMode));
}
public void execProducer(int n) {
for (int i = 0; i < n; i++) {
executorService.submit(new Producer(connection));
}
}
public void clearConfig(String[] queues) {
try {
final Channel channel = connection.createChannel();
channel.exchangeDelete(NORMAL_EXCHANGE);
channel.queueDelete(DEFAULT_WORKER_QUEUE);
for (String queue : queues) {
channel.queueDelete(queue);
}
System.out.println("Config cleared.");
System.exit(0);
} catch(IOException e) {
e.printStackTrace();
}
}
public static void main(String args[]) {
System.out.println("QS queue.");
if (args.length > 0) {
final String which = args[0];
if ("consumer".equals(which)) {
final boolean pushMode = (args.length > 1 ? "push".equals(args[1]) : true);
new QSQueue().execWorkderConsumer(pushMode);
} else if ("producer".equals(which)) {
final int n = (args.length > 1 ? Integer.parseInt(args[1]) : 1);
new QSQueue().execProducer(n);
} else if ("clearconfig".equals(which)) {
String[] queues = new String[0];
if (args.length > 1) {
queues = new String[args.length - 1];
System.arraycopy(args, 1, queues, 0, args.length - 1);
}
new QSQueue().clearConfig(queues);
}
}
}
}
其中,为了模拟consumer
处理一个重负载的消息, 调用了Thread.sleep
进行休眠操作.
将该类导出为Runnable JAR
包, 打开一个控制台终端,运行一个consumer
:
java -jar qsqueue.jar consumer
打开另一个控制台终端, 运行一个producer
:
java -jar qsqueue.jar producer
produce
中将发送10个消息, 由于consumer
中休眠的时间比较长, 可以通过RabbitMQ的management UI
查看服务器节点上的消息情况.一开始时, 显示如下信息:
也就是说, 有两个消息已经投递到了consumer
等待确认, 有八个消息处于等待投递状态.随着consumer对消息的确认,和RabbitMQ服务器不断的投递消息,服务器最终将处理完所有消息.
basicQos API
basicQos
API实在channel
实例上进行调用的, 该函数的签名为:
void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException
该函数API需要注意的几个细节有:
Envelope
结构的大小.当参数prefetchSize
设置为0时, 表示不对消息大小进行限制,即RabbitMQ服务器会投递尽可能多的消息到consumer.prefetchCount
的含义表示RabbitMQ服务器节点上最多的没有确认的消息个数. 这个参数设置为0时,表示不对消息个数进行限制, RabbitMQ服务器会投递尽可能多的消息到consumer.consumer
设置的prefetchSize
和prefetchCount
中最小满足条件进行消息投递. 示例中prefetchSize设置为0, prefetchCount设置为2, 则RabbitMQ会根据消息个数设置条件进行消息投递. 处理流程为:RabbitMQ向consumer投递消息#1和#2, 此时服务器上消息统计信息为Ready为8, Unacked为2, Total为10. 当consumer处理玩消息#1并向服务器确认后, 服务器将向consumer投递消息#3, 此时服务器上的消息统计信息为Ready为7, Unacked为2, Total为9.global
的含义是, 预取是channel
全局设置,还是在该channel上的每个consumer
上的设置.可以在channel上注册多个consumer, 但是因为消息在同一个channel中是顺序处理的, 因此这种场景实际意义并不大. 示例代码中取消注释掉的第二个consumer, 重新运行后, RabbitMQ第一次投递消息后的统计信息为: global
参数设置为false
, 每个consumer上最多有2个未确认消息, 而服务器节点上将最多有4个未确认消息.Pull 模式
可以使用basicGet
API获取RabbitMQ服务器节点上的消息,这个API每次获取一个消息. 但是该方式效率不高, 建议注册consumer, 使用push
模式对消息进行处理.另一个细节是, pull
模式的消息处理是在该API调用线程进行处理的, 而push
模式中, 消息在DeliverCallback
回调中处理, 这和consumer
的注册线程不是同一个线程.
小结:
当使用basicQos
API对push
模式的consumer
进行预取时, 如果consumer设置了自动确认, basicQos API不会起作用, 其没有任何意义.
BTW: 因为这个示例导出的Runnable JAR
包中包含了RabbitMQ的Java客户端库,所以可以直接运行. 否则,运行时需要在classpath
中指定其客户端库路径.