RabbitMQ(四) --消费者Consumer

·  阅读 5199

一:摘要概述

经过前面三篇文章的学习,对于RabbitMQ中间件应该处于拨开云雾见青天阶段。本文将趁热打铁,完善RabbitMQ基础应用最后一个消费版块。当然文中会持续深入讲解有关消息分发、消费端确认等中阶特性

二:消息消费

MQ队列可以理解为物品寄存中心,放进去总要拿出来用,一直放着没有利息还会持久增加成本引发系列问题。MQ存储的消息使用有两种途径,RabbitMQ服务推送、消费者客户端拉取

2.1 拉取消息

通过baiscGet()拉取RabbitMQ服务端消息有以下几点需要注意:

  • 一次只能消费一条消息,千万别使用用循环代替后面的baiscConsume()
  • 前面讲的队列创建参数有AutoDelete,但是注意这个自动删除前提为至少有一个消费者连接到队列,并且当所有消费者断开时删除,这里通过basicGet()消费不包含在内
        // 设置队列自动删除
        channel.queueDeclare("autoDeleteQueue", true, false, true, null);
        channel.basicPublish("", "autoDeleteQueue", null, "测试".getBytes());
        // 验证basicGet不触发队列自动删除
        channel.basicGet("autoDeleteQueue", true);
复制代码

当然basicGet()方法自身API比较简单,第一个参数指明消费队列,第二个参数设置是否自动应答即AutoAck。返回对象也就封装EnvelopeBasicPropertiesbody消息体等,具体信息如下表所示:

序号 方法参数 含义
1 queue 队列名称,指定消费者消费队列
2 autoAck 自动应答,打开为true后RabbitMQ应用送出消息将立即删除
序号 返回值 备注
1 envelope 包含deliveryTag、exchange、routingKey等信息
2 props BasicProperties对象,即消息生产时设置的该对象特性
3 body 消息体byte数组
4 messageCount 消息数量
2.2 推送消息

相对于拉取消息而言,basicConsume()推送消息更加符合生产环境的需求,持续监控消费队列。自然其API也更加复杂,常用系列重载参数如下表所示:

序号 方法参数 含义
1 queue 消费队列名称
2 autoAck 自动确认提交
3 consumerTag 消费者唯一标识
4 noLocal 不消费同一Connection连接生产的消息
5 consumer 具体组织消费逻辑对象,里面提供系列重载方法用户消费逻辑组装

推送消息消费最后一般都是采用实现Consumer接口亦或是继承DefaultConsumer类,DefaultConsumer实现了接口Consumer,但是大多数方法都是空实现,需要重写其中的逻辑。其中重要方法执行时间如下表所示:

        DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery (String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 处理消息逻辑
            }
        };
        channel.basicConsume("queueName",true,"consumerTag",defaultConsumer);
复制代码
序号 方法 执行时间
1 handleDelivery() 消费消息逻辑
2 handleConsumeOk() 第一篇文章就就讲到Consume-Ok命令在delivery命令前,即每个消费者开始消费前都会执行该方法一次
3 handleShutdownSignal() 当连接Connection / 信道Channel断开关闭时执行一次
4 handleRecoverOk() baiscRecover()命令队列重发未确认消息,当未确认消息被重发前执行这个方法
5 handleCancelOk() baiscCancel()显示取消消费者,当消费者被取消时指定这个方法
2.3 队列重发

上面讲到Consumer的方法handleRecoverOk()将会在消息重发时调用,显示的调用消息重发方法为basicRecover()。该方法只有一个参数:

  • true:表示可以将重发消息发送给其它消费者
  • false:表示只能将消息发送给相同的消费者
    在这里插入图片描述

三:消息确认

第一篇文章中有一个命令是Basic.Ack用于客户端向服务端反馈确认消息已经正常消费,当接收到命令后RabbityMQ服务端才会删除消息,从消费者客户端确保消息不会丢失。自然有确认就有拒绝确认,本节将介绍basicAck()、basicReject()、basicNack()

3.1 确认消费basicAck

RabbitMQ反馈确认消费通过命令basicAck()实现,该方法具备两个参数deliveryTagmultiple

	channel.basicAck(envelope.getDeliveryTag(),false);
复制代码
  • deliveryTag:确认消息的编号,这是每个消息被消费时都会分配一个递增唯一编号
  • multiple:批量确认,true表示所有编号小于目前确认消息编号的待确认消息都会被确认,false则只确认当前消息

特别注意:消息的编码是每个信道Channel范围的,批量确认操作也是针对当前Channel信道的操作。请务必记住这个范围

3.2 拒绝确认basicReject

程序在消费消息过程中抛出异常,或者是消息需要重复消费,这时候就可以将消费的消息拒绝确认。拒绝确认的消息有两种去处,删除、放回队列,通过参数requeue控制,拒绝确认的消息放回队列时会放置在队列首位,拒绝消息不放回队列可以搭配死信队列使用

	void basicReject(long deliveryTag, boolean requeue) throws IOException;
复制代码
3.3 拒绝确认basicNack

确认消费可以批量确认,为什么拒绝确认消息不能批量拒绝?所以为了补充basicReject()不足提出了basicNack()。这个API相对于basicReject()而言多了一个参数multiple,效果与批量确认一致

    void basicNack(long deliveryTag, boolean multiple, boolean requeue)
            throws IOException;
复制代码

四:消息预取

在这里插入图片描述
消息消费RabbitMQ采取的策略就是轮询机制,将每个消息发送给唯一的消费者。每个消费者获取到的消息都是平均的,这样的机制会导致下列问题:

  • 某些消息耗时超长,平均分配消息后可能导致某些消费者积压过多未消费消息,而同时某些消费者处于空闲状态,导致系统吞吐量下降

通过下面代码可以告诉RabbitMQ服务端,我只接受prefetchCount数量的未确认消息,当消费者客户端未确认消息达到限定值后服务端将不会给该消费者推送数据。第二个参数的含义如下表:

参数值 含义
false 默认值,单独应用于信道上所有消费者
true 信道上所有消费者共享
    void basicQos(int prefetchCount, boolean global) throws IOException;
复制代码

合理的消息预取配合消费端手动ACK确认机制可以很好的优化平衡消费者性能,这个预取数量问题可以根据队列消息增长率与消费端消息处理效率平衡

五:RPC

使用RabbitMQ完成RPC操作其实比较简单,就是使用了前面讲到过的BasicPeoperties对象。发送消息时消息可以携带该对象,前面使用到了对象的deliveryMod持久化、priority优先级、expiration自动过期删除属性。这里RPC将会使用到replyTo属性告诉RPC服务端执行完毕后回调队列地址,correlationId用于标识请求唯一ID

5.1 RPC客户端
  • UUID生成correlationId请求唯一标识ID,客户端消费回调队列时确认归属与自己的请求回调
  • ArrayBlockingQueue阻塞队列用于阻塞主线程等待RPC服务端完成逻辑以后的回调
  • 如果想限制RPC远程超时时间则可以在阻塞队列等待方法take()中添加最大等待时长
    @SneakyThrows
    public static void main (String[] args) {
        Channel channel = TemplateConfigServiceImpl.createChannel();
        // 创建BasicProperties赋值replyTo回调队列名称、correlationId请求唯一标识ID
        String correlationId = UUID.randomUUID().toString();
        String replyQueue = "queue1";
        AMQP.BasicProperties basicProperties = new AMQP.BasicProperties.Builder().replyTo(replyQueue).correlationId(correlationId).build();
        // 客户端向服务端监控队列发送消息
        String rpcQueue = "queueName";
        channel.basicPublish("",rpcQueue,basicProperties,"RPC测试".getBytes());
        // 创建阻塞队列等到消息回调
        ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
        // 监控回调队列消息获取远程调用结果
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery (String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 校验消息唯一标识是否匹配
                String replyCorrelationId = properties.getCorrelationId();
                long deliveryTag = envelope.getDeliveryTag();
                if (correlationId.equals(replyCorrelationId)){
                    // 将回调消息放到阻塞队列中
                    arrayBlockingQueue.offer(new String(body));
                    channel.basicAck(deliveryTag,false);
                }else {
                    // 不匹配消息放回队列
                    channel.basicReject(deliveryTag,true);
                }
            }
        };
        channel.basicConsume(replyQueue,false,"ConsumerTag",defaultConsumer);
        // 阻塞等待阻塞队列中消息处理后续逻辑
        String take = arrayBlockingQueue.take();
        System.out.println(take);
    }
复制代码
5.2 RPC服务端

整体RPC服务端、客户端实现都是最简陋的自行车设计,如果想要更复杂的逻辑请自行完成

    @SneakyThrows
    public static void main (String[] args) {
        Channel channel = TemplateConfigServiceImpl.createChannel();
        // 监控RPC队列消息执行任务
        String rpcQueue = "queueName";
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery (String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 执行计算逻辑
                System.out.println("RPC远程服务端开始执行任务");
                System.out.println(new String(body));
                // 组装回调消息
                String replyTo = properties.getReplyTo();
                channel.basicPublish("",replyTo,properties,"RPC远程计算任务完成".getBytes());
                // 确认消息消费
                long deliveryTag = envelope.getDeliveryTag();
                channel.basicAck(deliveryTag,false);
            }
        };
        channel.basicConsume(rpcQueue,false,"ConsumerTag",defaultConsumer);
    }
复制代码
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改