消费端手动确认消费

本文介绍了RabbitMQ消费端如何手动确认消息ACK和NACK,详细阐述了channel.basicAck和channel.basicNack的用法。在手动确认实现中,讨论了配置、编码实现及模拟消费失败的情况,强调了处理不当可能导致的消息丢失或死循环。还提到了Spring消息重试机制,以及在消费失败后的三种处理策略。最后,建议通常采用记录日志或入库方式处理消费失败,而非回退消息到队列。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

消费端 ACK 和 NACK

消费端消费消息的时候,出现了一般异常可以进行日志记录后续进行补偿处理,但是如果在处理消息时服务器出现了宕机,这时候就会丢失消息。rabbitmq 提供了手动签收机制,消费端向 broker 回复 ack 信号,确认消息是否已经消费。如果确认消费,broker 才会将消息删除,不确认消费的话,消费者恢复正常之后,broker 会再次投递该消息。此外,消费端处理异常的话,也可以回复 broker 一个 nack 信号将消息重回队列,broker 会再次投递该消息。

channel.basicAck
void basicAck(long deliveryTag, boolean multiple) throws IOException;
  • deliveryTag:该消息的index;
  • multiple:是否批量,如果为 true,将一次性ack所有小于deliveryTag的消息;确认收到消息。
channel.basicNack
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
  • deliveryTag:该消息的index;
  • multiple:是否批量,如果为 true,将一次性ack所有小于deliveryTag的消息;确认收到消息;
  • requeue:是否重回队列,true 重回队列,false 消息就直接丢弃了。

手动确认实现

添加配置
# 设置手动签收
spring.rabbitmq.listener.simple.acknowledge-mode=manual
编码实现
@Service
@Slf4j
public class MsgListener {
    @RabbitListener(queues = "testDirectRouting")
    public void listen(String object, Message message, Channel channel) {
        try {
            MessageProperties messageProperties = message.getMessageProperties();
            String correlationId = messageProperties.getCorrelationId();
            long deliveryTag = messageProperties.getDeliveryTag();
            log.info("消费成功:{},{},消息内容:{}", correlationId, deliveryTag, object);
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            log.error("签收失败", e);
            try {
                channel.basicNack(deliveryTag, false, true);
            } catch (IOException exception) {
                log.error("拒签失败", exception);
            }
        }
    }
}
模拟消费失败
@Service
@Slf4j
public class MsgListener {
    @RabbitListener(queues = "testDirectRouting")
    public void listen(String object, Message message, Channel channel) throws IOException {
        MessageProperties messageProperties = message.getMessageProperties();
        String correlationId = messageProperties.getCorrelationId();
        long deliveryTag = messageProperties.getDeliveryTag();
        log.info("消费成功:{},{},消息内容:{}", correlationId, deliveryTag, object);
        int a = 1 / 0;
        channel.basicAck(deliveryTag, false);
    }
}
org.springframework.amqp.rabbit.support.ListenerExecutionFailedException: Listener method 'public void com.skystep.rabbitmq.listener.MsgListener.listen(java.lang.String,org.springframework.amqp.core.Message,com.rabbitmq.client.Channel)' threw exception
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:229) ~[spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.invokeHandlerAndProcessResult(MessagingMessageListenerAdapter.java:149) ~[spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.onMessage(MessagingMessageListenerAdapter.java:134) ~[spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:1608) ~[spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.actualInvokeListener(AbstractMessageListenerContainer.java:1527) ~[spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:1515) ~[spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:1506) ~[spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.executeListener(AbstractMessageListenerContainer.java:1450) ~[spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.doReceiveAndExecute(SimpleMessageListenerContainer.java:975) [spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.receiveAndExecute(SimpleMessageListenerContainer.java:921) [spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$1600(SimpleMessageListenerContainer.java:83) [spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.mainLoop(SimpleMessageListenerContainer.java:1296) [spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1202) [spring-rabbit-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]
Caused by: java.lang.ArithmeticException: / by zero

以上代码,模拟业务出现异常,抛出异常。对于必须回复消费结果的消息来说,broker 并不会删除该消息,但是会导致消费者服务之后无法继续接收消息,直到重启服务。这是非常危险的操作,一旦生产者一直生产消息,那么将造成消息的严重堆积。

@Service
@Slf4j
public class MsgListener {
    @RabbitListener(queues = "testDirectRouting")
    public void listen(String object, Message message, Channel channel) {
        MessageProperties messageProperties = message.getMessageProperties();
        String correlationId = messageProperties.getCorrelationId();
        long deliveryTag = messageProperties.getDeliveryTag();
        log.info("消费成功:{},{},消息内容:{}", correlationId, deliveryTag, object);
        try {
            int a = 1 / 0;
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            log.error("处理业务失败", e);
            try {
                channel.basicNack(deliveryTag, false, true);
            } catch (IOException exception) {
                log.error("拒签失败", exception);
            }
        }
    }
}

以上代码,出现异常后执行 channel.basicNack ,第三个参数置为了 true,将消息重回了队列,那么该消息会回到队列的尾部,并且会被重新仍回消费端。通常代码的报错并不会因为重试就能解决,所以这个消息将会出现这种情况:继续被消费,继续报错,重回队列,继续被消费,继续报错,重回队列,进入死循环。但是如果第三个参数置为了 false 呢,则会导致消息直接丢失。

  • 当消费失败后将此消息存到 redis,如果消费了三次还是失败,记录消费次数,记录日志落库保存已被后期回溯;
  • 第三参数直接填 false ,不重回队列,记录日志、发送邮件等待开发手动处理;
  • 不启用手动 ack ,使用 SpringBoot 提供的消息重试。

以上三种,一般情况选择第二种,不过视最终场景选择决定。

Spring 消息重试

上面提到结合 SpringBoot 可以实现消息的重试。这个重试是 SpringBoot 提供的,重新执行消费者方法,而不是让 RabbitMQ 重新推送消息。

@Service
@Slf4j
public class MsgListener {
    @RabbitListener(queues = "testDirectRouting")
    public void listen(String object, Message message, Channel channel) {
        try {
            int a = 1 / 0; //故意报错测试
        } catch (Exception e) {
            log.error("签收失败", e);
            throw new RuntimeException("消息消费失败");
        }
    }
}

注意一定要手动 throw 一个异常,因为 SpringBoot 触发重试是根据方法中发生未捕捉的异常来决定的。值得注意的是这个重试是 SpringBoot 提供的,重新执行消费者方法,而不是让 RabbitMQ 重新推送消息。

总结

一般情况下都会使用消息手动签收确认的方式来消费消费消息。这样可以最大可能保证消息被消费。此外,一般不推荐消费失败后将消息回退队列。因为消费失败再次接收该消息还是相同的结果。推荐记录日志或者入库后续追溯、人工介入处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值