springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)

序言:

当数据通过生产者产生到发送到exchange交换器,再通过设定的路由规则,经过routingKey,最终会落地到queue 中。这个时候引出了Listener消费者了。在第一章中有对RabbitAnnotationDrivenConfiguration该类中源码做过基本分析,该类最大的作用就是通过配置文件的设定用于构建RabbitListenerContainerFactory(该接口的实现,工厂设计模式用以创建核心的MessageListenerContainer

容器类,而该类MessageListenerContainer用以管理MessageListener,最终实现的该接口类操作Connection最终来消费数据)

对于spring提供了两种工厂实现,SimpleRabbitListenerContainerFactory与DirectRabbitListenerContainerFactoryConfigurer,

1:SimpleRabbitListenerContainerFactory(SMLC)

用于构建SimpleMessageListenerContainer在章节一中也描述了该模式下,rabbitmq的消费模式,该监听容器通过客户端多线程来并行的处理消息,通过上述的配置设置并行度。也可以监控多个队列并在运行时对队列进行增删。系统通过 concurrentConsumers、maxConcurrentConsumers 灵活设定当前容器中消费者的数量,不论监控多少个队列,channel与当前线程都会一一对应(这也是为何命令为Simple的因素),即使用同一consumer线程来会处理所有的队列。即这里我们先使用该模式来进行消费。

1.1:通过Factory构建SimpleMessageListenerContainer

当设置完成,我们如果在应用中创建SimpleMessageListenerContainer,来监听queue呢。spring也提供了两种方式

yml配置:

listener:
      type: simple
      simple:
        ##这里使用none后续对此进行详细描述
        acknowledge-mode: none
        auto-startup: true
        ##设置消费并行度
        concurrency: 2
        ##设置最大并行度
        max-concurrency: 2
        ##设置一个批次最大拉取数量
        batch-size: 3
        ##不开启重试 避免重复消费
        retry:
          enabled: false
        ## 每个消费者可能未完成的未确认消息的最大数量
        #prefetch:
        ## 若容器中声明的的queue不存在了(queue在代理上不可用或在运行时被删除了) 是否需要停止对应容器默认true
        missing-queues-fatal: true
        ## 默认情况下,拒绝交付是否重新排队
        default-requeue-rejected: true
        # 应该多久发布一次空闲容器事件 可用于监控
        idle-event-interval: 10000

1.1.1: MessageListener

单条消费,可以通过配置开启自动ack或不开启ack


    @Autowired
    private SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory;


    //单条消费
    @Bean
    public MessageListenerContainer simpleMessageListenerContainer(){

        SimpleMessageListenerContainer listenerContainer = simpleRabbitListenerContainerFactory.createListenerContainer();

        //设置被监控的queue
        listenerContainer.setQueueNames(CommonConstant.queue_direct1,CommonConstant.queue_direct2);

        //等待消息到达超时时间默认1s
        listenerContainer.setReceiveTimeout(1000);
        //手动确认
        listenerContainer.setAcknowledgeMode(AcknowledgeMode.NONE);
        //默认10s
        listenerContainer.setStartConsumerMinInterval(10*1000);
        //默认60s
        listenerContainer.setStopConsumerMinInterval(60*1000);
        //设置消费者唯一标记 基于queue设置
        listenerContainer.setConsumerTagStrategy(queue -> {
            return queue+"_"+ UUID.randomUUID().toString();
        });

        listenerContainer.setMessageListener(message->{
            //获取数据
            byte[] body = message.getBody();
            String str = new String(body);

            Thread thread=Thread.currentThread();
            log.info("message:{}  ThreadId is:{}  ConsumerTag:{}  Queue:{}"
                    ,str,thread.getId(),message.getMessageProperties().getConsumerTag(),message.getMessageProperties().getConsumerQueue());

        });

        return listenerContainer;

    }

 上述设置了2个并行情况,有两个线程(consumer)并行消费

 

1.1.2: ChannelAwareMessageListener

包含channel与message可以进行手动ack确定

    @Autowired
    private SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory;

    @Bean
    public MessageListenerContainer simpleChannelMessageListenerContainer(){

        SimpleMessageListenerContainer listenerContainer = simpleRabbitListenerContainerFactory.createListenerContainer();

        //设置被监控的queue
        listenerContainer.setQueueNames(CommonConstant.queue_direct1,CommonConstant.queue_direct2);

        //等待消息到达超时时间默认1s
        listenerContainer.setReceiveTimeout(1000);
        //手动确认
        listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        //默认10s
        listenerContainer.setStartConsumerMinInterval(10*1000);
        //默认60s
        listenerContainer.setStopConsumerMinInterval(60*1000);
        //设置消费者唯一标记 基于queue设置
        listenerContainer.setConsumerTagStrategy(queue -> {
            return queue+"_"+ UUID.randomUUID().toString();
        });

        //包含channel
        listenerContainer.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {

            Thread thread=Thread.currentThread();
            long maxDeliveryTag= 0;

            //获取数据
            maxDeliveryTag = message.getMessageProperties().getDeliveryTag();
            //获取数据
            byte[] body = message.getBody();
            String str = new String(body);

            log.info("deliveryTag:{} message:{}  ThreadId is:{}  ConsumerTag:{}  Queue:{} channel:{}"
                    ,maxDeliveryTag,str,thread.getId(),message.getMessageProperties().getConsumerTag()
                    ,message.getMessageProperties().getConsumerQueue(),channel.getChannelNumber());

            //批量确认
            try {
                channel.basicAck(maxDeliveryTag,false);
            } catch (IOException e) {
                //确认失败 处理message数据需要回滚
                e.printStackTrace();
            }

        });

        return listenerContainer;

    }

 上述设置了2个并行情况,有两个线程(consumer)并行消费,并进行手动ack确定 ,法系爱你channel与消费线程为一一对应

 

1.1.3: BatchMessageListener

 @Bean
    public MessageListenerContainer batchMessageListenerContainer(){

        DirectMessageListenerContainer listenerContainer = directRabbitListenerContainerFactory.createListenerContainer();

        //设置被监控的queue
        listenerContainer.setQueueNames(CommonConstant.queue_direct1,CommonConstant.queue_direct2);

        //手动确认
        listenerContainer.setAcknowledgeMode(AcknowledgeMode.NONE);

        //为每个队列添加多个消费者 增加并行度
        listenerContainer.setConsumersPerQueue(3);

        //设置消费者唯一标记 基于queue设置
        listenerContainer.setConsumerTagStrategy(queue -> {
            return queue+"_"+ UUID.randomUUID().toString();
        });

        listenerContainer.setMessageListener((BatchMessageListener) messages->{
            //批量消费
            for (Message message:messages ) {
                //获取数据
                byte[] body = message.getBody();
                String str = new String(body);

                Thread thread=Thread.currentThread();
                log.info("message:{}  ThreadId is:{}  ConsumerTag:{}  Queue:{}"
                        ,str,thread.getId(),message.getMessageProperties().getConsumerTag(),message.getMessageProperties().getConsumerQueue());

            }

        });

        return listenerContainer;

    }

最终结果: 

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第1张图片

 

1.1.3: ChannelAwareBatchMessageListener

构建代码如下所示:

@Bean
    public MessageListenerContainer batchChannelMessageListenerContainer(){

        //设置开启批量处理
        simpleRabbitListenerContainerFactory.setConsumerBatchEnabled(true);
        simpleRabbitListenerContainerFactory.setBatchListener(true);

        SimpleMessageListenerContainer listenerContainer = simpleRabbitListenerContainerFactory.createListenerContainer();


        //设置被监控的queue
        listenerContainer.setQueueNames(CommonConstant.queue_direct1,CommonConstant.queue_direct2);

        //等待消息到达超时时间默认1s
        listenerContainer.setReceiveTimeout(1000);
        //手动确认
        listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        //默认10s
        listenerContainer.setStartConsumerMinInterval(10*1000);
        //默认60s
        listenerContainer.setStopConsumerMinInterval(60*1000);
        //设置消费者唯一标记 基于queue设置
        listenerContainer.setConsumerTagStrategy(queue -> {
            return queue+"_"+ UUID.randomUUID().toString();
        });

        listenerContainer.setMessageListener((ChannelAwareBatchMessageListener) (messages,channel)->{

            Thread thread=Thread.currentThread();

            long maxDeliveryTag= 0;

            //批量消费
            for (Message message:messages ) {
                //获取数据
                maxDeliveryTag = message.getMessageProperties().getDeliveryTag();
                //获取数据
                byte[] body = message.getBody();
                String str = new String(body);

                log.info("deliveryTag:{} message:{}  ThreadId is:{}  ConsumerTag:{}  Queue:{} channel:{}"
                        ,maxDeliveryTag,str,thread.getId(),message.getMessageProperties().getConsumerTag()
                        ,message.getMessageProperties().getConsumerQueue(),channel.getChannelNumber());
            }

            //批量确认
            try {
                channel.basicAck(maxDeliveryTag,true);
            } catch (IOException e) {
                //确认失败 处理message数据需要回滚
                e.printStackTrace();
            }
        });

        return listenerContainer;

    }

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第2张图片

最终发现设置2个并发下,存在2个Thread消费,并且2个Channel通道,其中deliverTag为每一条消费者中消息对应的id,因为是2个consumer,所以50(deliverTag)*2(2个consumer)*10(每个批次10条数据)=1000总数

2:使用@RabbitListener注解的方式

该方式的本质是springboot通过扫描@RabbitListener读取注解中设定的参数,去构建SimpleMessageListenerContainer,并进行监听,实际上1中繁琐的代码由spring框架来完成,极大的减少了业务的开发工作量,这种方式也是在实际工作中使用。

这里分析下springboot是如何做到:

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第3张图片

点开@EnableRabbit

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第4张图片

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第5张图片

 点进RabbitBootstrapConfiguration发现

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第6张图片

这里核心说的是关于RabbitListenerAnnotationBeanPostProcessor,该类就是真正进行注解扫描并初始化的

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第7张图片

 

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第8张图片

在processListener中

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第9张图片

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第10张图片

 

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第11张图片

实际使用:

/**
 * 使用注解方式进行消费
 * @author fangyaun
 */
@Component
@Slf4j
public class SimpleRabbitmqListenerAnnotation {


    /**
     *
     * @param message
     */
    @RabbitListener(containerFactory = "rabbitListenerContainerFactory",ackMode = "AUTO",
            queues = {CommonConstant.queue_direct1})
    public void handleMessage(String message){

        Thread thread=Thread.currentThread();
        log.info("==========auto-ack=======>message:{}  ThreadId is:{} "
                ,message,thread.getId());

    }


    /**
     * 手动确认
     *
     * @param message 需要处理消息
     * @param deliveryTag :使用@Header接口获取messageProperties中的DELIVERY_TAG属性。
     *
     */
    @RabbitListener(containerFactory = "rabbitListenerContainerFactory",ackMode = "MANUAL",
            queues = {CommonConstant.queue_direct2})
    public void handleMessage2(@Payload String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag){

        Thread thread=Thread.currentThread();
        log.info("==========ack=======>message:{}  ThreadId is:{}  channel:{}"
                ,message,thread.getId(),channel.getChannelNumber());

        try {
//            channel.basicNack(deliveryTag,false,true);
            channel.basicAck(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 
     * @param message
     * @param channel
     * @param deliveryTag
     */
    @RabbitListener(containerFactory = "rabbitListenerContainerFactory",ackMode = "MANUAL",
            queues = {CommonConstant.queue_direct2})
    public void handleMessage3(@Payload String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag){

        Thread thread=Thread.currentThread();
        log.info("==========nack=======>message:{}  ThreadId is:{}  channel:{}"
                ,message,thread.getId(),channel.getChannelNumber());

        try {
            
            channel.basicNack(deliveryTag,false,true);
//            channel.basicAck(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    
}

最终结果:

自动ack:

手动ack:

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第12张图片

 nack:

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第13张图片

 验证SimpleMessageListenerContainer上述的并发特性:即多个queue多个消费者下会公用一个channel。这里测试代码为2个queue,2个consumer.

  @RabbitListener(containerFactory = "rabbitListenerContainerFactory",ackMode = "MANUAL",
            queues = {CommonConstant.queue_direct2,CommonConstant.queue_direct1})
    public void handleMessage2(@Payload String message, Channel channel,@Header(AmqpHeaders.CONSUMER_QUEUE) String consumer_queue, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag){

        Thread thread=Thread.currentThread();
        log.info("==========ack=======>message:{}  ThreadId is:{}  channel:{} consumer_queue:{}"
                ,message,thread.getId(),channel.getChannelNumber(),consumer_queue);

        try {
            channel.basicAck(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

最终的结果:

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第14张图片

这也是为何上述会说到 一个 channel 要游走于多个 consumer 当中,这无疑增加了系统在上下文切换中的开销。并且一旦某个 channel 关闭或重启,意味着每个队列 queue 中使用当前 channel 的 consumer 都会受到影响

2:DirectRabbitListenerContainerFactoryConfigurer(DMLC)

DirectMessageListenerContainer: 与SMLC不同DMLC的并发性基于配置的队列和consumersPerQueue,每个队列的每个consumer都使用单独的channel(一个consumer消费线程会监控多个channel通道),并发性由Rabbit客户端库控制。默认情况下(基于当前版本),它使用DEFAULT_NUM_THREADS=Runtime.getRuntime().availableProcessors()(有效内核数) * 2线程池。可以通过配置taskExecutor提供所需的最大并发量。

DMLC的使用方式和SMLC大致相同,这里不主要叙说,这里只测试它的并发原理

 @Bean
    public MessageListenerContainer directChannelMessageListenerContainer(){

        DirectMessageListenerContainer listenerContainer = directRabbitListenerContainerFactory.createListenerContainer();

        //设置被监控的queue
        listenerContainer.setQueueNames(CommonConstant.queue_direct1,CommonConstant.queue_direct2);

        //手动确认
        listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);

        //为每个队列添加多个消费者 增加并行度
        listenerContainer.setConsumersPerQueue(2);

//        listenerContainer.setTaskExecutor();

        //设置消费者唯一标记 基于queue设置
        listenerContainer.setConsumerTagStrategy(queue -> {
            return queue+"_"+ UUID.randomUUID().toString();
        });

        //包含channel
        listenerContainer.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {

            Thread thread=Thread.currentThread();

            //获取数据
            long maxDeliveryTag = message.getMessageProperties().getDeliveryTag();
            //获取数据
            byte[] body = message.getBody();
            String str = new String(body);

            log.info("deliveryTag:{} message:{}  ThreadId :{} channel:{} ConsumerTag:{}  Queue:{} "
                    ,maxDeliveryTag,str,channel.getChannelNumber(),thread.getId(),message.getMessageProperties().getConsumerTag()
                    ,message.getMessageProperties().getConsumerQueue());

            //批量确认
            try {
                channel.basicAck(maxDeliveryTag,false);
            } catch (IOException e) {
                //确认失败 处理message数据需要回滚
                e.printStackTrace();
            }
        });

        return listenerContainer;

    }

springboot2.x +rabbitmq使用和源码分析四(消费者-Listener使用)_第15张图片

从测试代码以及最终结果可以验证出,每一个队列中的每个consumer(ThreadId)对应多个个channel,一个consumer消费线程会监控多个channel通道,所以在相同情况下DMLC的并发处理能力要高于SMLC,并且避免在RabbitMQ客户端线程和使用者线程之间进行上下文切换。


3:死信队列

死信队列(Dead-Letter-Exchange) ,从它的命令也可以看出它适用于存储已经死亡的消息的队列,那么什么情况下消息代表了死亡呢。

  1. 如果我们设置了消息的TTL(过期时间),在这个时间内该消息未被消费者消费,那么代表该消息以死亡
  2. 如果消费者拒绝了消息(在代码中执行了channel.basicReject (deliveryTag, requeue),或者channel.basicNack(deliveryTag,multiple, requeue) )
  3. 队列超出最大长度

从上述描述可以看出,基本上死信队列的产生都与消费者有关,所以也在此进行描述。死信队列也是一个正常的队列,只不过它存储的是从其它队列中转发过来的信息,所以我们仍然需要按照创建队列的方式创建它,唯一不同在于,我们需要通过x-dead-letter-exchange与x-dead-letter-routing-key两个header将需要监控的队列与死信队列进行绑定,如下所示:

@Bean
    public Queue directQueue1(){

        Map arguments  = new HashMap<>();
        
        //声明当前死信的 Exchange
        arguments.put("x-dead-letter-exchange",exchange_deadLetter_direct);

        arguments.put("x-dead-letter-routing-key",routingKey_deadLetter);

        return  new Queue(queue_direct1,true,false,false,arguments);
    }


    /**
     * 构建死
     * @return
     */
    @Bean
    public Queue deadLetterQueue(){

        return  new Queue(queue_deadLetter,true,false,false);
    }

    //绑定死信队列
    @Bean
    public Binding deadLetterBinding(){

        return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(routingKey_deadLetter).noargs();
    }

    @Bean
    public Exchange deadLetterExchange(){
        return new DirectExchange(exchange_deadLetter_direct);
    }

测试代码:

 @RabbitListener(containerFactory = "rabbitListenerContainerFactory",ackMode = "MANUAL",
            queues = {CommonConstant.queue_direct2,CommonConstant.queue_direct1})
    public void handleMessage2(@Payload String message, Channel channel,@Header(AmqpHeaders.CONSUMER_QUEUE) String consumer_queue, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {

        Thread thread=Thread.currentThread();
        log.info("==========ack=======>message:{}  ThreadId is:{}  channel:{} consumer_queue:{}"
                ,message,thread.getId(),channel.getChannelNumber(),consumer_queue);

          channel.basicReject(deliveryTag,false);
    }

最终的结果: 

Demo地址:https://github.com/fangyuan94/rabbitmq_demo.git 

你可能感兴趣的:(rabbitmq,SpringBoot)