RabbitMQ之客户端开发向导
上文学习过客户端开发中的创建连接、发送消息等操作,这里接着学习如何消费消息、消息确认以及关闭连接等。
客户端开发:消费消息、消息确认与拒绝、关闭连接
消费消息
在RabbitMQ中有两种消息处理的模式,一种是推模式/订阅模式/投递模式(也叫Push模式),消费者调用channel.basicConsume方法订阅队列后,由RabbitMQ主动将消息推送给订阅队列的消费者;另一种是拉模式/检索模式(也叫Pull模式),需要消费者调用channel.basicGet方法,主动从指定队列中拉取消息。
- 推模式:消息中间件主动将消息推送给消费者,采用 Basic.Consume 进行消费
- 拉模式:消费者主动从消息中间件拉取消息,调用 Basic.Get 进行消费
推模式
在推模式中,可以通过持续订阅的方式来消费消息。接收消息一般通过实现Consumer接口或者继承DefaultConsumer接口来实现。相关的类有:
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
**当调用与Consumer相关的API方法时,不同的订阅采用不同的消费者标签(consumerTag)来区分彼此,在同一个Channel中消费者也需要通过唯一的消费者标签来区分。**以下是关键的消费代码:
boolean autoAck = false;
channel.basicQos(64);
channel.basicConsume(queueName, autoAck, "myConsumerTag",new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException{
String routingKey = envelope.getRoutingKey();
String contentType = properties.getContentType();
long deliveryTag = envelope.getDeliveryTag();
// todo…… 在这里处理消息
// (process the message components here ……)
/** 此处进行显示的 ack 操作(channel.basicAck),是防止消息不必要的丢失。*/
channel.basicAck(deliveryTag, false);
}
});
Channel 类中 basicConsume 方法有如下几种形式:
public String basicConsume(String queue, Consumer callback) throws IOException {
return this.basicConsume(queue, false, callback);
}
public String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException {
return this.basicConsume(queue, autoAck, "", callback);
}
public String basicConsume(String queue, boolean autoAck, String consumerTag, Consumer callback) throws IOException {
return this.basicConsume(queue, autoAck, consumerTag, false, false, (Map)null, callback);
}
public String basicConsume(String queue, boolean autoAck, Map<String, Object> arguments, Consumer callback) throws IOException {
return this.basicConsume(queue, autoAck, "", false, false, arguments, callback);
}
public String basicConsume(String queue, boolean autoAck, String consumerTag, boolean noLocal, boolean exclusive, Map<String, Object> arguments, Consumer callback) throws IOException {
String result = this.delegate.basicConsume(queue, autoAck, consumerTag, noLocal, exclusive, arguments, callback);
this.recordConsumer(result, queue, autoAck, exclusive, arguments, callback);
return result;
}
其对应的参数说明如下:
①queue:队列的名称。
②autoAck:设置是否自动确认。建议设置成false,即不自动确认。
③consumerTag:消费者标签,用来区分多个消费者。
④noLocal:设置为 true 则表示不能将同一个 Connection 中生产者发送的消息传送给这个Connection 中的消费者。
⑤exclusive:设置是否排他。
⑥arguments:设置消费者的其他参数。
⑦callback:设置消费者的回调函数。用来处理 RabbitMQ 推送过来的消息,比如DefaultConsumer,使用时需要客户端重写(override)其中的方法。
对于Consumer来说重写handleDelivery方法也是十分方便的。更复杂的Consumer会重写(override)更多的方法,具体如下:
void handleConsumeOk (String consumerTag);
void handleCancelOk (String consumerTag);
void handleCancel (String consumerTag) throws IOException;
void handleShutdownSignal (String consumerTag, ShutdownSignalException sig);
void handleRecoverOk (String consumerTag);
比如handleShutdownSignal方法,当channels和connections关闭的时候会调用,handleConsumeOk在其他callback方法之前调用,返回消费者标签。
Consumer同样可以重写handleCancelOk和handleCancel方法,这样在显示的或者隐式的取消的时候调用。也可以通过Channel.basicCancel方法显示的cancel一个指定的Consumer:
channel.basicCancel(consumerTag);
注意,上面代码首先触发handleConsumerOk,之后触发handleDelivery方法,最后触发handleCancelOk方法。
生产者和消费者客户端都需要考虑线程安全的问题。消费者客户端的callback会被分配到与Channel 不同的线程池上,这就意味着消费者客户端可以安全的调用这些阻塞方法,比如channel.queueDeclare、channel.basicCancel 等。
每个Channel都拥有自己独立的线程。最常用的做法是一个Channel 对应一个消费者,也就是说消费者之间彼此没有任何联系。当然也可以在一个Channel 中维持多个消费者,但是需要注意一个问题,如果Channel 中的一个消费者一直在运行,那么其他消费者的callback 会被 “耽搁”。
以下是推模式的时序图:

拉模式
拉模式的消费方式是通过Channel.basicGet可以一个一个的获取消息,其返回值是GetResponse。Channel 类的basicGet 方法没有其他重载方法,仅有:
public GetResponse basicGet(String queue, boolean autoAck) throws IOException {
return this.delegate.basicGet(queue, autoAck);
}
其中的queue代表队列名称,如果将autoAck 设置为 false ,则需要调用channel.basicAck 来确认消息已被成功接收。
拉模式的关键代码如下:
GetResponse response = channel.basicGet(QUEUE_NAME, false);
System.out.println(new String(response.getBody()));
channel.basicAck(response.getEnvelope().getDeliveryTag(),false);
拉模式的时序图如下:

注意:Basic.Consume 将信道(Channel)置为投递模式,直到取消队列的订阅为止。在投递模式期间,RabbitMQ 会不断的推送消息给消费者,当然推送消息的个数还是会受到 Basic.Qos的限制。如果只是想从队列获取单条消息而不是持续订阅,建议还是使用Basic.Get 进行消费。但是不能将Basic.Get 放在一个循环里来代替 Basic.Consume,这样做会严重影响RabbitMQ 的性能。如果要实现高吞吐量,消费者应该使用 Basic.Consume 方法。
两种模式优缺点对比:
推模式:将消息提前推送给消费者,消费者必须设置一个缓冲区缓存这些消息。好处是消费者总是有一堆在内存中待处理的消息,所以效率高。缺点是缓冲区可能会溢出。
拉模式:在消费者需要时才去消息中间件拉取消息,这段网络开销会明显增加消息延迟,降低系统吞吐量。
因此,在选择推模式还是拉模式时,需要考虑实际的使用场景,然后权衡选择。
消费端的确认与拒绝
为了保证消息从队列可靠地到达消费者,RabbitMQ提供了消息确认机制。 消费者在订阅队列时,可以指定autoAck参数:
- autoAck为false时,RabbitMQ会等待消费者显示地回复确认信号
- autoAck为true时, RabbitMQ会自动把发送出去的消息置为确认
- 然后才从内存(或者磁盘)中移除消息(实际上是打上删除标记,再删除)。
采用消息确认机制之后,只要设置autoAck参数为false,消费者就有足够的时间处理消息,不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会等待直到显示调用Basic.Ack命令
当autoAck参数被设置为false时,对于RabbitMQ服务端而言,队列里的消息分为两种:一部分是等待投递给消费者的消息,一部分是已经投递给消费者等待回复的消息。如果RabbitMQ一直没有收到消费者的确认信号,并且消费此消息的消费者断开连接,RabbitMQ会安排此消息重新进入队列。
RabbitMQ判断消息是否需要重新投递给消费者的唯一依据:消费该消息的消费者连接是否已经断开。
RabbitMQ查看队列中投递给消费者但未收到确认信号的消息数:
root@node01:/# rabbitmqctl list_queues name messages_unacknowledged
Listing queues ...
hello 0
queue_test 0
logs_queue 1
work_demo 0
在消费者接收到消息后,如果想明确拒绝当前的消息而不是确认,该怎么做呢?RabbitMQ在2.0.0版本之后引入了Basic.Reject命令,消费者客户端可以调用与其对应的channel.basicReject方法来告诉RabbitMQ拒绝这个消息。
Channel类中的basicReject方法定义如下:
public void basicReject(long deliveryTag, boolean requeue) throws IOException {
this.delegate.basicReject(deliveryTag, requeue);
}
其中:
- deliveryTag: 可以看做消息的编号,它是一个64位的长整型值。如果requeue设置为true,则RabbitMQ会将这条消息存入队列,以便可以发送给下一个订阅的消费者;如果requeue为false,则RabbitMQ就会立即把消息从队列中移除,而不会发送给新的消费者。
**Basic.Reject命令一次只能拒绝一条消息,如果想要批量拒绝消息,则使用Basic.Nack这个命令。**消费者客户端可以调用channel.basicNack方法来实现,方法定义如下:
public void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException {
this.delegate.basicNack(deliveryTag, multiple, requeue);
}
其中deliveryTag和requeue的含义可以参考basicReject方法。multiple参数设置为false则表示拒绝编号为deliveryTag的这条消息,这时候basicNack和basicReject方法一样;miltiple参数设置为true则表示拒绝deliveryTag编号之前所有未被当前消费者确认的消息。
注意:将channel.basicReject或者channel.basicNack中的requeue设置为false, 可以启用死信队列的功能。死信队列可以通过检测
被拒绝或者未送达的消息来追踪问题。
对于requeue,AMQP还有一个命令Basic.Recover具备可重入队列的特性。对应的客户端方法为:
public RecoverOk basicRecover() throws IOException {
return this.delegate.basicRecover();
}
public RecoverOk basicRecover(boolean requeue) throws IOException {
return this.delegate.basicRecover(requeue);
}
这个方法用来请求RabbitMQ重新发送还未被确认的消息。
- 如果requeue为true, 则未被确认的消息会被重新加入队列,同一条消息,可能会被分配给之前不同的消费者。
- 如果requeue为false, 则同一条消息会分配给之前的消费者。默认情况下,如果不设置requeue参数,则channel.basicRecover(true),即默认为true。
关闭连接
在应用程序使用完之后,需要关闭连接,释放资源:
channel.close();
connection.close();
显式地关闭Channel 是个好习惯,但这不是必须的,在Connection关闭的时候,Channel 也会自动关闭。
AMQP协议中的Connection和Channel采用同样的方式来管理网络失败、内部错误和显式地关闭连接。Connection 和Channel 所具备的生命周期如下所述:
- Open : 开启状态,代表当前对象可以使用。
- Closing : 正在关闭状态。当前对象被显式地通知调用关闭方法( shutdown) ,这样就产生了一个关闭请求让其内部对象进行相应的操作, 并等待这些关闭操作的完成。
- Closed : 已经关闭状态。当前对象己经接收到所有的内部对象己完成关闭动作的通知,并且其也关闭了自身。
Connection 和Channel最终都是会成为Closed 的状态,不论是程序正常调用的关闭方法,或者是客户端的异常,再或者是发生了网络异常。
在Connection 和Channel 中,与关闭相关的方法有addShutdownListener(ShutdownListener listener) 和removeShutdownListener (ShutdownListnerlistener) 。当Connection 或者Channel的状态转变为Closed 的时候会调用ShutdownListener 。而且如果将一个ShutdownListener 注册到一个己经处于Closed状态的对象(这里特指Connection 和Channel 对象)时,会立刻调用ShutdownListener 。
getCloseReason方法可以让你知道对象关闭的原因;isOpen方法检测对象当前是否处于开启状态;close(int closeCode , String closeMessage)方法显式地通知当前对象执行关闭操作。
有关ShutDownListener的使用可以参考如下代码:
connection.addShutdownListener(new ShutdownListener() {
public void (ShutdownSignalException cause) {
......
}});
// channel 添加监听器
channel.addShutdownListener(new ShutdownListener() {
public void (ShutdownSignalException cause) {
......
}});
// 移除监听器
connection.removeShutdownListener(listener);
channel.removeShutdownListener(listener);
当触发ShutdownListener的时候,就可以获取到ShutdownSignalException ,这个ShutdownSignalException 包含了关闭的原因,这里原因也可以通过调用前面所提及的getCloseReason 方法获取。
ShutdownSignalException 提供了多个方法来分析关闭的原因:
isHardError方法可以知道是Connection的还是Channel的错误;getReason方法可以获取cause 相关的信息
相关代码示例如下:
public void shutdownCompleted(ShutdownSignalException cause) {
if(cause.isHardError()){
Connection conn = (Connection)cause.getReference();
if(!cause.isInitiatedByApplication()){
Method reason = cause.getReason();
...
}
...
}else{
Channel ch = (Channel)cause.getReference();
...
}
}
参考资源:
- https://blog.youkuaiyun.com/u013256816/article/details/71342274
- https://blog.youkuaiyun.com/weixin_41922349/article/details/102373950
- https://blog.youkuaiyun.com/ITWANGBOIT/article/details/105428281
- https://blog.youkuaiyun.com/qq_34579060/article/details/94742205
- https://www.cnblogs.com/no-celery/p/14014891.html
- 《RabbitMQ实战指南》 朱忠华 著
1万+

被折叠的 条评论
为什么被折叠?



