在 RocketMQ里消费方式虽有PUSH与PULL两种,但实现机制实为 PULL 模式,PUSH 模式是一种伪推送,是对 PULL 模式的封装,每拉去一批消息后,提交到消费端的线程池(异步),然后马上向 Broker 拉取消息,即实现类似“推”的效果。下面是消息拉取示意图:

image.png

在 RocketMQ 中绝大数场景中,通常会选择使用 PUSH 模式,具体原因下方也会进行说明。

下面分别介绍下两者的关联。

一,PUSH与PULL分别是什么

  • PUSH指的是客户端与服务端建立好网络长连接,当服务端有数据时立即通过连接将数据推送给客户端。
  • PULL指的是客户端主动向服务端请求,拉取数据。

二,PUSH与PULL的特点

  • PUSH的一个特点是及时,一旦有数据服务端立即将数据推送给客户端;对客户端来说比较友好,无须处理无数据的情形;不过服务端并不知道客户端的处理能力,如果客户端处理能力低会造成消息堆积在客户端的问题。
  • PULL因为是客户端主动去服务端拉取数据,所以不存在消息堆积问题;但什么时候有数据客户端是无法感知的,所以拉取时间间隔不好控制,间隔长消息消费不及时;间隔短会出现无效拉取的请求。
    在PULL模式下为了保证消费的实时性,采起了长轮询消息服务器拉取消息的方式,每隔一定时间客户端向服务端发起一次请求,如果有数据则取回进行消费,如果服务端没数据客户端线程会阻塞,阻塞时间为15S,有数据了就会被唤醒。长轮询还是由consumer发起的,因此就算broker端有大量数据也不会主动推送给consumer。
    关于长轮询的实现在PullRequestHoldService类里。

三,PUSH与PULL的实现

先看看PULL使用的一个示例:

import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class PullConsumerTest {
    public static void main(String[] args) throws Exception {
        Semaphore semaphore = new Semaphore();
        Thread t = new Thread(new Task(semaphore));
        t.start();
        CountDownLatch cdh = new CountDownLatch(1);
        try {
            //程序运行 120s
            cdh.await(120 * 1000, TimeUnit.MILLISECONDS);
        } finally {
            semaphore.running = false;
        }
    }
    /**
     * 消息拉取核心实现逻辑
     */
    static class Task implements Runnable {
        Semaphore s = new Semaphore();
        public Task(Semaphore s ) {
            this.s = s;
        }
        public void run() {
            try {
                DefaultMQPullConsumer consumer = new 
                    DefaultMQPullConsumer("dw_pull_consumer");
                consumer.setNamesrvAddr("127.0.01:9876");
                consumer.start();
                Map<MessageQueue, Long> offsetTable = new HashMap<MessageQueue, Long>();
                Set<MessageQueue> msgQueueList = consumer.
                    fetchSubscribeMessageQueues("TOPIC_TEST"); // 获取该 Topic 的所有队列
                if(msgQueueList != null && !msgQueueList.isEmpty()) {
                    boolean noFoundFlag = false;
                    while(this.s.running) {
                        if(noFoundFlag) { // 没有找到消息,暂停一下消费
                            Thread.sleep(1000);
                        }
                        for( MessageQueue q : msgQueueList ) {
                            PullResult pullResult = consumer.pull(q, "*",                                          decivedPulloffset(offsetTable
                             , q, consumer) , 3000);
                            System.out.println("pullStatus:" + 
                                               pullResult.getPullStatus());
                            switch (pullResult.getPullStatus()) {
                                case FOUND:
                                    doSomething(pullResult.getMsgFoundList());
                                    break;
                                case NO_MATCHED_MSG:
                                    break;
                                case NO_NEW_MSG:
                                case OFFSET_ILLEGAL:
                                    noFoundFlag = true;
                                    break;
                                default:
                                    continue ;
                            }
                            //提交位点
                            consumer.updateConsumeOffset(q, 
                                 pullResult.getNextBeginOffset());
                        }
                        System.out.println("balacne queue is empty: " + consumer.
                              fetchMessageQueuesInBalance("TOPIC_TEST").isEmpty());
                    }
                } else {
                    System.out.println("end,because queue is enmpty");
                }
                consumer.shutdown();
                System.out.println("consumer shutdown");
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }
    /** 拉取到消息后具体的处理逻辑 */
    private static void doSomething(List<MessageExt> msgs) {
        System.out.println("本次拉取到的消息条数:" + msgs.size());
    }
    public static long decivedPulloffset(Map<MessageQueue, Long> offsetTable, 
             MessageQueue queue, DefaultMQPullConsumer consumer) throws Exception {
        long offset = consumer.fetchConsumeOffset(queue, false);
        if(offset < 0 ) {
            offset = 0;
        }
        System.out.println("offset:" + offset);
        return offset;
    }
    static class Semaphore {
        public volatile boolean running = true;
    }
}

消息的拉取实现主要在任务 Task 的 run 方法中,重点看下:

  1. 首先根据 MQConsumer 的 fetchSubscribeMessageQueues 的方法获取 Topic 的所有队列信息
  2. 然后遍历所有队列,依次通过 MQConsuemr 的 PULL 方法从 Broker 端拉取消息。
  3. 对拉取的消息进行消费处理
  4. 通过调用 MQConsumer 的 updateConsumeOffset 方法更新位点,但需要注意的是这个方法并不是实时向 Broker 提交,而是客户端会启用以线程,默认每隔 5s 向 Broker 集中上报一次。

上面的示例逻辑倒是挺清晰,不过以下这些问题我们在使用时需要考虑的:

  1. 从broker拉取了一批消息后多个消费者需要手动完成队列的分配。上例是只是一个消费组且组里只有一个消费者,如果是多个我们需要考虑队列的分配情况
  2. 消费完消息后我们需要主动上报消费进度,然后拉取下一批。
  3. 如果遇到消息消费失败,需要告知 Broker,该条消息消费失败,后续需要重试,通过手动调用 sendMessageBack 方法实现

说明下,在MQPullConsumer这个类里面,有一个MessageQueueListener,它的目的就是当queue发生变化的时候,通知Consumer。也正是这个接口帮助我们在Pull模式里面实现负载均衡。

/**
 * A MessageQueueListener is implemented by the application and may be specified when a message queue changed
 */
public interface MessageQueueListener {
    /**
     * @param topic message topic
     * @param mqAll all queues in this message topic
     * @param mqDivided collection of queues,assigned to the current consumer
     */
    void messageQueueChanged(final String topic, final Set<MessageQueue> mqAll,
        final Set<MessageQueue> mqDivided);
}

我们再来看一下PUSH使用的示例

public static void main(String[] args) throws InterruptedException, MQClientException {
        DefaultMQPushConsumer consumer = new 
            DefaultMQPushConsumer("dw_test_consumer_6");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe("TOPIC_TEST", "*");
        consumer.setAllocateMessageQueueStrategy(new 
               AllocateMessageQueueAveragelyByCircle());
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                ConsumeConcurrentlyContext context) {
                try {
                    System.out.printf("%s Receive New Messages: %s %n", 
                          Thread.currentThread().getName(), msgs);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                } catch (Throwable e) {
                    e.printStackTrace();
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
            }
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }

理下流程:

  1. 首先 new DefaultMQPushConsumer 对象,并指定一个消费组名。
  2. 然后设置相关参数,例如 nameSrvAdd、消费失败重试次数、线程数等
  3. 通过调用 setConsumeFromWhere 方法指定初次启动时从什么地方消费,默认是最新的消息开始消费。
  4. 通过调用 setAllocateMessageQueueStrategy 指定队列负载机制,默认平均分配。
  5. 通过调用 registerMessageListener 设置消息监听器,即消息处理逻辑,最终返回 CONSUME_SUCCESS(成功消费)或 RECONSUME_LATER(需要重试)。

相较于PULL方式,我们在使用PUSH方式时只需指定好相关策略然后在MessageListener的回调里进行消息处理就行。PUSH消息方式由于返回了消息的状态,服务端会维护每个消费端的消费进度,内部会记录消费进度,消息发送成功后会更新消费进度。另外队列的负载我们也无须干预太多,这些问题都被封装了。

本文主要介绍了消费的两种方式分别为PULL与PUSH,也介绍了二者的特点,然后分别给出了相应的示例,再简单总结下:

  • PULL: 消费者订阅主题,然后自动进行集群内消息队列的动态负载,自动拉取消息。准实时。
  • PUSH:消费者无需订阅主题,由业务方(应用程序)直接根据MessageQueue拉取消息。

相较于PULL方式,PUSH方式封装了更多,使用起来对用户更友好,大数场景通常会选择使用 PUSH 的方式。

参考文章:08 消息消费 API 与版本变迁说明
【RocketMq实战第四篇】-不同类型消费者(DefaultMQPushConsumer&DefaultMQPullConsumer)
RocketMQ消息消费方式 推拉模式
RocketMQ推拉模式


步履不停
38 声望13 粉丝

好走的都是下坡路