【RabbitMQ】消息队列中间件学习之RabbitMQ(4)


上文学习过客户端开发中的创建连接、发送消息等操作,这里接着学习如何消费消息、消息确认以及关闭连接等。

客户端开发:消费消息、消息确认与拒绝、关闭连接

消费消息

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参数:

  1. autoAck为false时,RabbitMQ会等待消费者显示地回复确认信号
  2. autoAck为true时, RabbitMQ会自动把发送出去的消息置为确认
  3. 然后才从内存(或者磁盘)中移除消息(实际上是打上删除标记,再删除)。

采用消息确认机制之后,只要设置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();
        ...
    }
}

参考资源:

  1. https://blog.youkuaiyun.com/u013256816/article/details/71342274
  2. https://blog.youkuaiyun.com/weixin_41922349/article/details/102373950
  3. https://blog.youkuaiyun.com/ITWANGBOIT/article/details/105428281
  4. https://blog.youkuaiyun.com/qq_34579060/article/details/94742205
  5. https://www.cnblogs.com/no-celery/p/14014891.html
  6. 《RabbitMQ实战指南》 朱忠华 著
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

镰刀韭菜

看在我不断努力的份上,支持我吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值