RabbitMQ系列--消费者的消息确认机制

1,417 阅读5分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

RabbitMQ消费者消息确认的三种机制:
  1. 自动确认(AcknowledgeMode.NONE)
  • RabbitMQ消费者默认为自动确认,不会管消费者是否成功消费/处理了消息
  1. 根据情况确认(AcknowledgeMode.AUTO)
  • 如果消息成功被消费(成功的意思是在消费的过程中没有抛出异常),则自动确认
  • 当抛出 AmqpRejectAndDontRequeueException 异常的时候,则消息会被拒绝,且 requeue = false(不重新入队列)
  • 当抛出 ImmediateAcknowledgeAmqpException 异常,则消费者会被确认
  • 其他的异常,则消息会被拒绝,且 requeue = true(如果此时只有一个消费者监听该队列,则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的)。可以通过 setDefaultRequeueRejected(默认是true)去设置
  1. 手动确认(AcknowledgeMode.MANUAL)
  • 消费者收到消息后,手动对消息进行处理,完成消费
  • Basic.Ack :用于确认当前消息
  • Basic.Nack :用于否定当前消息
  • Basic.Reject :用于拒绝当前消息
消息接收配置类

记得先把之前的监听类都注释掉

package com.chentawen.rabbitmqconsumer.config;

import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * 消息接收配置类
 * @author admin
 */
@Configuration
public class MessageListenerConfig {

    /**
     * RabbitTemplate 连接工厂
     */
    @Resource
    private CachingConnectionFactory connectionFactory;

    /**
     * 消息接收处理类
     */
    @Resource
    private MyAckReceiver myAckReceiver;
 
    @Bean
    public SimpleMessageListenerContainer simpleMessageListenerContainer() {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        // 消费者数量,默认10
        container.setConcurrentConsumers(1);
        // 每个消费者获取最大投递数量 默认50
        container.setMaxConcurrentConsumers(1);

        // 自动确认消息
//        container.setAcknowledgeMode(AcknowledgeMode.NONE);
        // 根据情况确认消息
//        container.setAcknowledgeMode(AcknowledgeMode.AUTO);
        // 手动确认消息
//        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        //设置一个队列
        container.setQueueNames("MyDirectQueue");

        //如果同时设置多个如下: 前提是队列都是必须已经创建存在的
        //  container.setQueueNames("MyDirectQueue","MyDirectQueue2","MyDirectQueue3");
 
 
        //另一种设置队列的方法,如果使用这种情况,那么要设置多个,就使用addQueues
        //container.setQueues(new Queue("MyDirectQueue",true));
        //container.addQueues(new Queue("MyDirectQueue2",true));
        //container.addQueues(new Queue("MyDirectQueue3",true));
        container.setMessageListener(myAckReceiver);
 
        return container;
    }
 
}
消息接收处理类
package com.chentawen.rabbitmqconsumer.config;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * 消息接收处理类
 * @author admin
 */
@Component
class MyAckReceiver implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        // 唯一标识 ID
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            String msg = message.toString();
//            System.out.println("msg: " + msg);
//            System.out.println("------------------------------");

            Map<String, String> msgMap = stringToMap(msg);
            String messageId = msgMap.get("messageId");
            String messageData = msgMap.get("messageData");
            String createTime = msgMap.get("createTime");

            System.out.println("MyAckReceiver  messageId: " + messageId + "  messageData: " + messageData + "  createTime: " + createTime);
            System.out.println("------------------------------");
            /**
             * 确认消息,参数说明:
             * long deliveryTag:唯一标识 ID
             * boolean multiple:是否批处理,当该参数为 true 时,则可以一次性确认 deliveryTag 小于等于传入值的所有消息
             */
//            channel.basicAck(deliveryTag, true);

            /**
             * 否定消息,参数说明:
             * long deliveryTag:唯一标识 ID
             * boolean multiple:是否批处理,当该参数为 true 时,则可以一次性确认 deliveryTag 小于等于传入值的所有消息
             * boolean requeue:如果 requeue 参数设置为 true 时,则 RabbitMQ 会重新将这条消息存入队列,以便发送给下一个订阅的消费者
             * 如果 requeue 参数设置为 false,则 RabbitMQ 立即会还把消息从队列中移除,而不会把它发送给新的消费者
             */
            //channel.basicNack(deliveryTag, true, false);
        } catch (Exception e) {
            e.printStackTrace();

            /**
             * 拒绝消息,参数说明:
             * long deliveryTag:唯一标识 ID
             * boolean requeue:如果 requeue 参数设置为 true 时,则 RabbitMQ 会重新将这条消息存入队列,以便发送给下一个订阅的消费者
             * 如果 requeue 参数设置为 false 时,则 RabbitMQ 立即会还把消息从队列中移除,而不会把它发送给新的消费者
             */
//            channel.basicReject(deliveryTag, true);
        }
    }

    /**
     * 这是未经处理的消息内容:
     * (Body:'{createTime=2021年08月17日 21:53:42, messageId=753047b6-3435-44e9-8382-318ea7913944, messageData=Hello World!}'
     * MessageProperties [headers={spring_listener_return_correlation=b14787e0-6920-4135-9a42-044f0c4cb638},
     * contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT,
     * priority=0, redelivered=false, receivedExchange=MyDirectExchange, receivedRoutingKey=DirectRoutingKey,
     * deliveryTag=3, consumerTag=amq.ctag-Lh7_hz1siXgqXFh8bMxLBw, consumerQueue=MyDirectQueue])
     * 将接收到的消息转换为map格式
     * @param str
     * @return
     */
    private static Map<String, String> stringToMap(String str) {
        Map<String, String> map = new HashMap<>(16);
        String[] split = str.split("'");
        String data = split[1];
        String[] split2 = data.substring(1, data.length() - 1).split(",");
        for (String s : split2) {
            String[] strings = s.split("=");
            map.put(strings[0].trim(), strings[1].trim());
        }
        return map;
    }

}

先注释掉 消息接收配置类 确认消息方式以及 消息接收配置类 手动处理的代码(上面的代码已注释),启动生产者和消费者的项目,发送一条消息查看控制台的打印结果(上面的代码监听的是MyDirectQueue)

image.png 可以发现消费者默认是自动确认消息的 image.png

根据情况确认消息的触发条件上面已经详细说明了,因为用的比较少就不演示了,项目中用的较多的是手动确认的方式

在 消息接收配置类 中修改确认消息方式为手动确认,消息接收配置类中 打开手动确认消息的代码,查看控制台打印信息,大家自行进行测试

这里给大家看下测试发生异常,消息重新存入队列发送给下一个消费者

  • 首先直接在消息接收处理类中加上异常(throw new Exception("测试异常"))
  • 捕捉异常后手动拒绝消息(channel.basicReject(deliveryTag, true))

image.png

  • 创建一个监听类(监听MyDirectQueue)
@Component
@RabbitListener(queues = "MyDirectQueue")
public class DirectReceiver {

    @RabbitHandler
    public void process(Map MessageData) {
        System.out.println("rabbitmq-consumer1接收到消息  : " + MessageData.toString());
    }

}
  • 重启项目发送消息,查看控制台 可以发现抛出了异常后,消息重新进入了队列,刚刚创建的消息者进行了消费 image.png
消费来自不同队列的消息进行业务处理

记得注释刚刚及以前的消费者,以免被抢先消费了

  • 在消息配置类中添加监听队列
//如果同时设置多个如下: 前提是队列都是必须已经创建存在的
container.setQueueNames("MyDirectQueue", "MyTopicQueueA","MyTopicQueueB");

image.png

  • 在消息接受处理类添加业务处理代码
if ("MyDirectQueue".equals(message.getMessageProperties().getConsumerQueue())) {
    System.out.println("消费的消息来自的队列:" + message.getMessageProperties().getConsumerQueue());
    System.out.println("消费来自MyDirectQueue的消息,进行对应业务处理...");
    System.out.println("------------------------------");
}

if ("MyTopicQueueA".equals(message.getMessageProperties().getConsumerQueue())) {
    System.out.println("消费的消息来自的队列:" + message.getMessageProperties().getConsumerQueue());
    System.out.println("消费来自MyTopicExchange.A的消息,进行对应业务处理...");
    System.out.println("------------------------------");
}

if ("MyTopicQueueB".equals(message.getMessageProperties().getConsumerQueue())) {
    System.out.println("消费的消息来自的队列:" + message.getMessageProperties().getConsumerQueue());
    System.out.println("消费来自MyTopicExchange.B的消息,进行对应业务处理...");
    System.out.println("------------------------------");
}

image.png

  • 重启项目,分别发送消息给两个队列,查看控制台 可以对来自不同队列的消息进行相应的业务处理

image.png

以上就是本期内容,后续内容持续更新