5.消息消费模式与拉取模式


highlight: arduino-light

4.1.2 消费消息模式

1)负载均衡模式(默认)

一个group:保证一条消息只会被同一个Group中的一个消费者消费。

多个group:一条消息只会被每个Group中的一个消费者消费。

1.一条消息只会被同一个Group中的一个Consumer消费,同一个Group多个消费者共同消费队列消息,每个消费者处理的消息不同。

2.多个Group同时消费一个Topic的同一条数据,每个Group都会有一个Consumer可以消费到这条数据

默认使用的是集群消费模式

java this.messageModel = MessageModel.CLUSTERING;

java public static void main(String[] args) throws Exception {    // 实例化消息生产者,指定组名    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");    // 指定Namesrv地址信息.    consumer.setNamesrvAddr("localhost:9876");    // 订阅Topic    consumer.subscribe("tyrant", "*");    //负载均衡模式消费 默认就是负载均衡消费模式    consumer.setMessageModel(MessageModel.CLUSTERING);    // 注册回调函数,处理消息    consumer.registerMessageListener(new MessageListenerConcurrently() {        @Override        public ConsumeConcurrentlyStatus consumeMessage           (List<MessageExt> msgs,ConsumeConcurrentlyContext context) {            System.out.printf("%s Receive New Messages: %s %n",                              Thread.currentThread().getName(), msgs);            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;       }   });    //启动消息者    consumer.start();    System.out.printf("Consumer Started.%n"); }

2)广播模式

java 同一个Group下的各个 Consumer 实例都消费一遍,如某个应用集群部署,有3个实例,每个实例都会消费一次,需要幂等。 ​ 即使这些 Consumer 属于同一个Group ,消息也会被Group 中的每个 Consumer 都消费一次。 ​ 多个Group也是一样 消息也会被Group 中的每个 Consumer 都消费一次。

public static void main(String[] args) throws Exception {    // 实例化消息生产者,指定组名    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");    // 指定Namesrv地址信息.    consumer.setNamesrvAddr("localhost:9876");    // 订阅Topic    consumer.subscribe("tyrant", "*");    // 广播模式消费    consumer.setMessageModel(MessageModel.BROADCASTING);    // 注册回调函数,处理消息    consumer.registerMessageListener(new MessageListenerConcurrently() {        @Override        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,                                                    ConsumeConcurrentlyContext context) {            System.out.printf("%s Receive New Messages: %s %n",                              Thread.currentThread().getName(), msgs);            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;       }   });    //启动消息者    consumer.start();    System.out.printf("Conumer Started.%n"); }

4.1.3 消费消息-推拉

数据交互有两种模式:Push(推模式)、Pull(拉模式)。

推模式:及时+可能导致积压

推模式指的是客户端与服务端建立好网络长连接,服务方有相关数据,直接通过长连接通道推送到客户端。

优点是及时,一旦有数据变更,客户端立马能感知到;另外对客户端来说逻辑简单,不需要关心有无数据这些逻辑处理。

缺点是不知道客户端的数据消费能力,可能导致数据积压在客户端,来不及处理。

拉模式:不及时+不积压

拉模式指的是客户端主动向服务端发出请求,拉取相关数据。

优点:是此过程由客户端发起请求,拉取的频率和速度都是消费者自己控制。故不存在推模式中数据积压的问题。

缺点:是可能不够及时,对客户端来说需要考虑数据拉取相关逻辑,何时去拉,拉的频率怎么控制等等。

RocketMq的推拉

目前绝大部分的MQ都是基于PULL模式。

RocketMQ消息消费本质上也是基于拉(pull)模式,consumer主动向消息服务器broker拉取消息。

consumer被分为2类:MQPullConsumer和MQPushConsumer。

本质都是拉模式,即consumer轮询从broker拉取消息。

区别:MQPushConsumer方式,consumer把轮询过程封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。

主要用的也是这种方式。

推模式中,为了保证消息消费的实时性,采取了长轮询消息服务器拉取消息的方式。每隔一定时间,客户端向服务端发起一次请求,服务端有数据就返回数据,服务端如果此时没有数据,保持hold住连接。等到有数据返回(相当于一种push),或者超时返回。长轮询Pull的好处就是可以减少无效请求,保证消息的实时性,又不会造成客户端积压。

同时推模式中Rokcet也做了一定的限流处理!

MQPullConsumer方式,取消息的过程需要用户自己写,首先通过打算消费的Topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。

DefaultMQPushConsumer,由系统控制读取操作,收到消息后自动调用传入的处理方法来处理 DefaultMQPullConsumer,读取操作中的大部分功 能由使用者自主控制 。

下面详细讲讲Rocket的推模式

DefaultMQPushConsumer

实现了 MQConsumerInner接口

RocketMQ未真正实现消息推模式,而是消费者主动向消息服务器拉取消息,RocketMQ推模式是循环向消息服务端发起消息拉取请求,如果消息消费者向RocketMQ拉取消息时,消息未到达消费队列。

大家只需记住长轮询就是在Broker在没有新消息的时候才阻塞,阻塞时间默认设置是 15秒,有消息会立刻返回

RocketMQ通过在Broker客户端配置longPollingEnable为true来开启长轮询模式。默认就是开启。

如果没有启用长轮询机制,则会在服务端等待shortPollingTimeMills(默认1秒)时间后即挂起,再去判断消息是否已经到达指定消息队列,如果消息仍未到达则提示拉取消息客户端PULL—NOT—FOUND即消息不存在。

如果开启长轮询模式,RocketMQ一方面会每隔5s轮询检查一次消息是否到达,一有消息达到后立马通知挂起线程再次验证消息是否是自己感兴趣的消息,如果是则从CommitLog文件中提取消息返回给消息拉取客户端,否则直到挂起超时,超时时间由消息拉取方在消息拉取是封装在请求参数中,PUSH模式为15s,即3次以后没有消息到达则立刻返回null。PULL模式通过DefaultMQPullConsumer#setBrokerSuspendMaxTimeMillis设置。

使用 DefaultMQPushConsumer 主要是设置好各种参数和传入处理消息的函数 。 系统收到消息后自动调用处理函数来处理消息,自动保存 Offset,而且加入新的 DefaultMQPushConsumer后会自动做负载均衡。

有兴趣的可以看一下长轮询源码在package org.apache.rocketmq.broker.longpolling.PullRequestHoldService.java的 run方法,里面会进行三次Check,每次5s。如果15秒没有消息返回。立刻返回null。然后重新进行下一轮的拉取。

因为拉取消息构建拉取请求的时候,brokerSuspendMaxTimeMillis默认是15秒。

java private static final long BROKER_SUSPEND_MAX_TIME_MILLIS = 1000 * 15; this.pullAPIWrapper.pullKernelImpl(                pullRequest.getMessageQueue(),                subExpression,                subscriptionData.getExpressionType(),                subscriptionData.getSubVersion(),                pullRequest.getNextOffset(),                this.defaultMQPushConsumer.getPullBatchSize(),                sysFlag,                commitOffsetValue,                BROKER_SUSPEND_MAX_TIME_MILLIS,                CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,                CommunicationMode.ASYNC,                pullCallback           );

java public PullResult pullKernelImpl(        final MessageQueue mq,        final String subExpression,        final String expressionType,        final long subVersion,        final long offset,        final int maxNums,        final int sysFlag,        final long commitOffset,        final long brokerSuspendMaxTimeMillis,        final long timeoutMillis,        final CommunicationMode communicationMode,        final PullCallback pullCallback   ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {         ​            PullMessageRequestHeader requestHeader = new PullMessageRequestHeader();            requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis);            //发送请求去拉取消息            PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage(                brokerAddr,                requestHeader,                timeoutMillis,                communicationMode,                pullCallback); ​            return pullResult;       } ​        throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);   }

服务端处理请求:org.apache.rocketmq.broker.processor.PullMessageProcessor#processRequest()

java private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend){  switch (response.getCode()) { case ResponseCode.PULL_NOT_FOUND: ​                    if (brokerAllowSuspend && hasSuspendFlag) {                        long pollingTimeMills = suspendTimeoutMillisLong;                        if (!this.brokerController.getBrokerConfig().isLongPollingEnable()) {                            pollingTimeMills = this.brokerController.getBrokerConfig()                               .getShortPollingTimeMills();                       } ​                        String topic = requestHeader.getTopic();                        long offset = requestHeader.getQueueOffset();                        int queueId = requestHeader.getQueueId();                        //pullMessage函数的参数是 final PullRequest pullRequest 。                        PullRequest pullRequest = new PullRequest                           (request, channel, pollingTimeMills,                           this.brokerController.getMessageStore().now(),                             offset, subscriptionData, messageFilter);                        this.brokerController.getPullRequestHoldService()                           .suspendPullRequest(topic, queueId, pullRequest);                        response = null;                        break;                   } } ​ }

java @Override    public void run() {        log.info("{} service started", this.getServiceName());        while (!this.isStopped()) {            try {               //启用了长轮询 调用await等5秒                if (this.brokerController.getBrokerConfig().isLongPollingEnable()) {                    this.waitForRunning(5 * 1000);               } else {                    //没有启用长轮询 调用await等1秒                    this.waitForRunning(this.brokerController                         .getBrokerConfig()                         .getShortPollingTimeMills());               } ​                long beginLockTimestamp = this.systemClock.now();                //检查是否有消息                this.checkHoldRequest();                long costTime = this.systemClock.now() - beginLockTimestamp;                if (costTime > 5 * 1000) {                    log.info("[NOTIFYME] check hold request cost {} ms.", costTime);               }           } catch (Throwable e) {                log.warn(this.getServiceName() + " service has exception. ", e);           }       } ​        log.info("{} service end", this.getServiceName());   }

长轮询

Push的方式是 Server端接收到消息后,主动把消息推送给 Client端,主动权在Server端,实时性高。用 Push方式主动推送有很多弊端:首先是加大 Server 端的 工作量,进而影响 Server 的性能;其次,Client 的处理能力各不相同, Client 的状态不受 Server 控制,

Pull方式是 Client端循环地从 Server端拉取消息,主动权在 Client手里, 自己拉取到一定量消息后,处理妥当了再接着取。Pull 方式的的弊端:循环拉取 消息的间隔不好设定,间隔太短就处在一个 “忙等”的状态,浪费资源; Pull 的时间间隔太长 Server 端有消息到来时 有可能没有被及时处理。

“长轮询”方式通过 Client端和 Server端的配合,达到既拥有 Pull 的优 点,又能达到保证实时性的目的 。

这是通过“长轮询”方式达到 Push效果的方法,长轮询方式既有 Pull 的优点,又兼具 Push方式的实时性。

长轮询的主动权还是掌握在 Consumer 手中, Broker 即使有大量消息积 压,也不会主动推 送给 Consumer

流量控制

DefaultMQPushConsumer 的流量控制

上面我们分析得知PushConsumer的核心还是 Pull 方式。PushConsumer有个线程池 , 消息处理逻辑在各个线程里同时执行。

多线程处理业务是很麻烦的,所以RocketMQ定义了一个快照类 ProcessQueue来解决堆积的数量 ?如何重复处理某些消息? 如何延迟处理某些消息? 等问题。每个 Message Queue 都会有个对应的 ProcessQueue 对象,保存了这个 Message Queue 消息处理状态的快照 。

ProcessQueue对象里主要的内容是一个 TreeMap 和一个读写锁。 TreeMap 里以 Message Queue 的 Offset作为 Key,以消息内容的引用为 Value ,保存了 所有从 MessageQueue 获取到,但是还未被处理的消息; 读写 锁控制着多个线程对 TreeMap 对象的并发访 问 。

有 了 ProcessQueue 对象,流量控制就方便和灵活多了,客 户 端在每次 Pull请求前会做几个判断,分别取还未处理的消息个数、消 息总大小、 Offset 的跨度,任何一个值超过设定的大小就隔一段时间再拉取消 息,从而达到流量控制的目的 。 此外 ProcessQueue 还可以辅助实现顺序消费的 逻辑。

有兴趣的可以翻看源码位置在org.apache.rocketmq.client.impl.consumer.pullMessage()

网上有很多文章讲处理流程的,个人觉得这个比较详细:

https://blog.csdn.net/panxj856856/article/details/80776032

把defaultMQPushConsumerImpl.start()方法按顺序都分析了。

不过我要说的重点是消息处理逻辑是在pullMessage这个函数的PullCallBack中。PullCallBack函数里有个 switch 语句,根据从 Broker 返回的消息类型做相应的 处理。

代码示例

java package rocketmq.base.consumer; ​ import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; ​ import java.util.List; ​ /** * 消息的接受者 */ public class Consumer { ​    public static void main(String[] args) throws Exception {        //1.创建消费者Consumer,制定消费者组名        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");        //2.指定Nameserver地址        consumer.setNamesrvAddr("localhost:9876");        //3.订阅主题Topic和Tag        consumer.subscribe("tyrant", "tag"); ​        //设定消费模式:广播模式        consumer.setMessageModel(MessageModel.CLUSTERING); ​        //4.设置回调函数,处理消息        consumer.registerMessageListener(new MessageListenerConcurrently() { ​            //接受消息内容            @Override            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {                for (MessageExt msg : msgs) {                    System.out.println("consumeThread=" + Thread.currentThread().getName() + "," + new String(msg.getBody()));               }                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;           }       });        //5.启动消费者consumer        consumer.start();   } }

DefaultMQPullConsumer

实现了 MQConsumerInner接口

```java public class PullConsumer {    private static final Map OFFSE_TABLE = new HashMap ();

   public static void main(String[] args) throws MQClientException {        DefaultMQPullConsumer consumer            = new DefaultMQPullConsumer("pleaserenameuniquegroupname_5");

       consumer.start();

       Set mqs = consumer.fetchSubscribeMessageQueues("TopicTest1");        for (MessageQueue mq : mqs) {            System.out.printf("Consume from the queue: %s%n", mq);            SINGLE MQ:            while (true) {                try {                    PullResult pullResult =                        consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);                    System.out.printf("%s%n", pullResult);                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());                    switch (pullResult.getPullStatus()) {                        case FOUND:                            break;                        case NOMATCHED MSG:                            break;                        case NONEW MSG:                            break SINGLEMQ;                        case OFFSET_ILLEGAL:                            break;                        default:                            break;                   }               } catch (Exception e) {                    e.printStackTrace();               }           }       }

       consumer.shutdown();   }

   private static long getMessageQueueOffset(MessageQueue mq) {        Long offset = OFFSE_TABLE.get(mq);        if (offset != null)            return offset;

       return 0;   }

   private static void putMessageQueueOffset(MessageQueue mq, long offset) {        OFFSE_TABLE.put(mq, offset);   }

} ```

选择master还是slave拉取

1.rocketmq的读写分离架构,写请求通过写的topic和messagequeue定位对应broker集群的master节点,读请求通过消费topic指定的messagequeue找到broker集群的master节点,在根据messagequeue对应多个consumerqueue和自己的offset找到对应的consumerqueue,在根据consumerqueue的offset找到对应的commitlog来进行消费

2 .broker写性能的提升基于commit log的顺序追加写默认大小1g和commit log先写入os cache在通过异步线程刷盘落地对应的consumerqueue也如此。

读性能提升基于零拷贝也就是直接从os cache中读取数据,不需要将数据重新加载到broker的内存中在通过socket来传输直接通过os cache和网卡进行数据传输。

3.读数据时如果写远远超过读导致os cache中放不下较旧的消息异步刷盘到了磁盘上。则从磁盘读取消息来消费,同时master会根据offset来判断消费者落后多少以及自己当时的负载让消费者去读取slave节点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值