RabbitMQ的消息确认机制

399 阅读5分钟

一、RabbitMQ消息确认机制

RabbitMQ的消息确认有两种:
1、对生产端发送消息的确认。这种是用来确认生产者将消息发送给交换机,交换机递给队列的过程中,消息是否成功投递。发送确认分为两步,一是确认是否到达交换机,二是确认是否到达队列。
2、对消费端消费消息的确认。这种是确认消费者是否成功消费了队列中的消息。

二、RabbitMQ对生产端发送消息的确认

 rabbitmq对生产端发送消息的确认分为事务和实现confirm机制。不过一般不使用事务,性能消耗太大。

三、消费端消费消息后对RabbitMQ的确认

为了保证消息能可靠到达消费端,RabbitMQ也提供了消费端的消息确认机制。消费者在声明队列时,可以指定noAck参数,当noAck=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(或磁盘,如果是持久化消息的话)中移除消息。否则,RabbitMQ会在队列中消息被消费后立即删除它。
采用消息确认机制后,只要令noAck=false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直持有消息直到消费者显式调用basicAck为止。

消费端消息的确认分为:自动确认(默认)、手动确认、不确认

AcknowledgeMode.NONE:不确认
AcknowledgeMode.AUTO:自动确认
AcknowledgeMode.MANUAL:手动确认

手动确认在spring-boot中配置方法:
spring.rabbitmq.listener.simple.acknowledge-mode = manual

1、消费成功手动确认方法:
void basicAck(long deliveryTag, boolean multiple) throws IOException;
deliveryTag:该消息的index
multiple:是否批量确认。true:将一次性ack所有小于deliveryTag的消息。
消费者成功处理消息后,手动调用channel.basicAck(message.getMessageProperties().getDeliveryTag(), false)方法对消息进行消费确认。

try {
      channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); // 手动确认消息
      System.out.println("投递消息确认成功,tag:"+message.getMessageProperties().getDeliveryTag());
} catch (IOException e) {
      e.printStackTrace();
}

2、消费失败手动确认方法:
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
deliveryTag:该消息的index。
multiple:是否批量. true:将一次性拒绝所有小于deliveryTag的消息。
requeue:被拒绝的是否重新入队列。

void basicReject(long deliveryTag, boolean requeue) throws IOException;
deliveryTag:该消息的index。
requeue:被拒绝的是否重新入队列。

channel.basicNack 方法与 channel.basicReject 方法区别在于basicNack可以批量拒绝多条消息,而basicReject一次只能拒绝一条消息。

四、消费者手动确认可能出现的问题

1、 消息无法ack

消费端在消费消息过程中出现异常,不能回复ack应答,消息将变成unacked状态,并且一直处于队列中。如果积压的过多将会导致程序无法继续消费数据。
消费端服务重启,断开rabbitmq的连接后,unacked的消息状态会重新变为ready等待消费。但是如果不重启消费端服务,消息将一直驻留在MQ中。

所以,可以捕获异常,然后调用Nack确认,然后消息进入队列重新消费。

2、无效消息循环重入队列

在上一个问题中,如果消费端捕获异常,并进行basicNack应答,并将消息重新放入队列中,可能会出现另一个问题:

如果消息或者代码本身有bug,每次处理这个消息都会报异常,那消息将一直处于消费——>报异常——>重入队列——>继续消费——>报异常。。。的死循环过程。 

以上两个问题其实属于同一类问题,都需要我们确保代码在消费消息后,一定要通知MQ,不然消息将一直驻留在MQ中。如果消息成功消费,则调用channel.basicAck正常通知mq;如果消费失败,则调用channel.basicNack或者channel.basicReject确认消费失败。

但防止死循环有两种处理办法:

1、根据处理过程中报的不同异常类型,选择消息要不要重入队列。

enum Action {
  ACCEPT,  // 处理成功
  RETRY,   // 可以重试的错误
  REJECT,  // 无需重试的错误
}

Action action = Action.RETRY; 
try {
    // 如果成功完成则action=Action.ACCEPT
}
catch (Exception e) {
   // 根据异常种类决定是ACCEPT、RETRY还是 REJECT
}
finally {
  // 通过finally块来保证Ack/Nack会且只会执行一次
  if (action == Action.ACCEPT) {
    channel.basicAck(tag);
  } else if (action == Action.RETRY) {
     channel.basicNack(tag, false, true);
  } else {
     channel.basicNack(tag, false, false);
  }  
} 

2、将处理失败的消息放入另一个队列中,手动取出处理。

五、 rabbitmq的unacked 问题

ack的设置

springboot项目 如果没有指定 rabbitmq 的应答方式,默认是自动应答,这样即使程序在处理消息 process过程中出现异常,这个消息也是被消费掉的。

为了保证消息的可靠性这里推荐采用手动方式应答,即通过代码实现。
ack配置

spring.rabbitmq.listener.acknowledge-mode: MANUAL

手动签收,并回馈信息给MQ

Long deliverTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);

channel.basicAck(deliverTag, false);

消息unack 的异常出现场景

image.png

如若处理过程中出现异常,而没有回复ack 应答。通过后台就会看到有 unacked 的数据。

如果积压的多会导致程序无法继续消费数据(数量和消费者的线程数有关)。
解决办法 针对异常 做处理,捕捉到后 也回复ack应答。

程序断开于rabbitmq的链接后 unacked的消息状态会重新变为ready 等待消费。
代码更新后,server应用连接rabbitmq 就会重新消费掉消息。