RocketMQ系列之push(推)消息模式(六)

前言

推消息的常用示例

public class Consumer {

    public static void main(String[] args) throws InterruptedException, MQClientException {
			
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

        consumer.subscribe("TopicTest", "*");
        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");
    }
}

推模式和拉模式在使用上有很大的不同,其中实现机制最大的不同在于DefaultMQPushConsumer

我们首先来看一下rocketMq推拉模式实现的主要类图

其实MQConsumer上面还有一个顶级类MQAdmin,没有画出来。

首先我们看一下MQConsumer的代码实现

/**
 * Message queue consumer interface
 */
public interface MQConsumer extends MQAdmin {
    /**
     * If consuming failure,message will be send back to the brokers,and delay consuming some time
     */
    @Deprecated
    void sendMessageBack(final MessageExt msg, final int delayLevel) throws RemotingException,
        MQBrokerException, InterruptedException, MQClientException;

    /**
     * 如果消费失败,消息会被重新发送到borker, 并且延迟消费。
     */
    void sendMessageBack(final MessageExt msg, final int delayLevel, final String brokerName)
        throws RemotingException, MQBrokerException, InterruptedException, MQClientException;

    /**
     * 获取消息队列从订阅的topic上
     *
     * @param topic message topic
     * @return queue set
     */
    Set<MessageQueue> fetchSubscribeMessageQueues(final String topic) throws MQClientException;
}

###PullMessageService

介绍拉模式的消息,那么必须要讲一下PullMessageService , 该类是一个线程类,继承了ServiceThread这个对象,其实就是一个线程。

下面看一下ServiceThread 的大致实现,隐藏了一些暂时不需要了解的代码,不然代码太长了

public abstract class ServiceThread implements Runnable {
		private Thread thread;
    protected final CountDownLatch2 waitPoint = new CountDownLatch2(1);
    protected volatile AtomicBoolean hasNotified = new AtomicBoolean(false);
    protected volatile boolean stopped = false;
    protected boolean isDaemon = false;

    //Make it able to restart the thread
    private final AtomicBoolean started = new AtomicBoolean(false);

    public ServiceThread() {

    }

    public abstract String getServiceName();

    public void start() {
        log.info("Try to start service thread:{} started:{} lastThread:{}", getServiceName(), started.get(), thread);	
      	// 更新启动状态,看是否重复启动
        if (!started.compareAndSet(false, true)) {
            return;
        }
      
      	// 默认是不停止的
        stopped = false;
      	// 初始化一个线程,runnable就是当前的类
        this.thread = new Thread(this, getServiceName());
      	// 是否是守护线程, 默认不是守护线程,JVM关闭的时候,需要等当前线程执行完毕
        this.thread.setDaemon(isDaemon);
      	// 启动啦。。。
        this.thread.start();
    }
}

看一下PullMessageService 的大致实现

public class PullMessageService extends ServiceThread {
	// 省略....
  @Override
    public void run() {
        log.info(this.getServiceName() + " service started");

        while (!this.isStopped()) {
            try {
              	// 从队列里面获取请求信息
                PullRequest pullRequest = this.pullRequestQueue.take();
               // 获取消息
                this.pullMessage(pullRequest);
            } catch (InterruptedException ignored) {
            } catch (Exception e) {
                log.error("Pull Message Service Run Method exception", e);
            }
        }

        log.info(this.getServiceName() + " service end");
    }
  // 省略....
}

大致的代码层次就是下面这样子的

------Runnable

----------ServiceThread

--------------PullMessageService

下面看一下MQClientInstance初始化创建的时候的代码,删除了一些不属于本章节的内容。

public void start() throws MQClientException {
        synchronized (this) {
            switch (this.serviceState) {
                case CREATE_JUST:
            				// 省略源码------
                    // 开启pullService , 针对consumer
                    this.pullMessageService.start();
               			// 省略源码------
                    break;
                case RUNNING:
                    break;
                case SHUTDOWN_ALREADY:
                    break;
                case START_FAILED:
                    throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
                default:
                    break;
            }
        }
    }

在MQClientInstance启动的时候,会调用pullMessageService的start的方法,其实就是调用到了ServiceThread的start方法。

好了,上面我们已经清楚了什么是PullMessageService, 在这里做一下总结。

PullMessageService 就是一个后台线程,消费者启动这个线程用来获取去borker获取消息的。

这个类里面有个很重要的东西,就是一个阻塞队列

public class PullMessageService extends ServiceThread {
    private final LinkedBlockingQueue<PullRequest> pullRequestQueue = new LinkedBlockingQueue<PullRequest>();
} 

这个队列非常重要,该队列里面存储的是有去获取消息的请求, 这个时候,就有人连环三问了

什么时间

什么地点

什么人

会往pullRequestQueue 里面放元素呢?

OK,带着这个问题,笔者调试了一波代码,一顿操作猛如虎,总算搞了个头绪出来。

  1. 程序启动的时候, 首先往这个队列里面放元素的是负载均衡器,org.apache.rocketmq.client.impl.consumer.RebalanceImpl

,负载均衡器会根据负载均衡算法计算出当前消费者需要消费的队列, 每一个队列构建一个pullRequest , 然后放入队列

  1. 每次请求消息之后,会把pullRequest 重新放入队列, 以供接下来继续请求
  2. 程序在运行过程中,负载均衡器检测到有新增加的队列,那么会把新增加的队列作为一个pullRequest 放入队列
提问: pullRequest什么时候会被移除?

答: pullRequest里面有一个很重要的概念ProcessQueue , 这个是请求处理队列,该队列里面会维护好一个 dropped 变量,当变成false的时候,这个pullRequest从对立里面取出来之后,就不会再放回去了,相当于被移除了。 一般在Rebalance 的时候进行移除换句话说,其实就是当消费者数量发生变化或者topic队列发生变化的时候,通过负载均衡器重新进行负载的时候,如果这个queue不再被本Consumer消费,那么其所对应的pullRequest会被移除

public class PullRequest {
  	// 当前消费组
		private String consumerGroup;
  	// 消息队列信息
		private MessageQueue messageQueue;
  	// 接下来要消费的offSet进度
    private long nextOffset;
  	// 当前队列信息
    private ProcessQueue processQueue;
    private boolean lockedFirst = false;
}

那么接下来看一下push模式的消息拉取是什么样子的。

消息拉取

上一个段落里面,pullMessageService里面从队列获取了一个pullRequest , 然后调用了pullMessage 方法,下面看一下这个方法

org.apache.rocketmq.client.impl.consumer.PullMessageService

private void pullMessage(final PullRequest pullRequest) {
  			// 从MQClientFactory里面获取消费者信息
        final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
        if (consumer != null) {
          	// 这里强转给PushConsumer, 如果不是的话,那么肯定就报错了,所以这里做了一个限制
            DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
          	// 走一把
            impl.pullMessage(pullRequest);
        } else {
            log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
        }
    }

步骤说明:

  1. 从MQClientFactory里面获取消费者信息, 这个在DefaultMQPushConsumer 初始化的时候,注册进去的。 当时注册的是DefaultMQPushConsumerImpl
  2. 强转为DefaultMQPushConsumerImpl, 如果不是的话,那么肯定就报错了,所以这里做了一个限制
  3. 调用DefaultMQPushConsumerImplpullMessage方法
pullMessage

源码入口:org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl

public void pullMessage(final PullRequest pullRequest) {
        // 从pullRequest中获取处理队列,每个RocketMq的queue被消费者消费的时候,都会指定一个pullRequest ,
        // 那么这个queue拉取到的缓存消息,消息统计等信息,都在`ProcessQueue`里面被包含着。
        final ProcessQueue processQueue = pullRequest.getProcessQueue();
        // 判断当前pullRequest是否被丢弃
        if (processQueue.isDropped()) {
            log.info("the pull request[{}] is dropped.", pullRequest.toString());
            return;
        }
        // 设置pullRequest处理队列最后一次的处理时间
        pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());

        try {
            //   检查服务的状态是否OK, 也就是是否在Running状态
            this.makeSureStateOK();
        } catch (MQClientException e) {
            log.warn("pullMessage exception, consumer state not ok", e);
            // 将pullRequest放入队列,只不过是通过后台的定时线程池延迟放入,防止一直请求
            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
            return;
        }
        // 消费者是否被暂停,如果被暂停的话,则暂时返回
        if (this.isPause()) {
            log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
            // 将pullRequest放入队列,只不过是通过后台的定时线程池延迟放入,防止一直请求
            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
            return;
        }
        // 当前pullRequest总共缓存了多少消息
        long cachedMessageCount = processQueue.getMsgCount().get();
        // 计算缓存消息的大小, 以MB为单位
        long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
        // pullThresholdForQueue = 1000  , 缓存在消费端的消息最多为1000条,如果大于,那么这次就放弃拉取,等待下次
        if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
            // 将pullRequest放入队列,只不过是通过后台的定时线程池延迟放入,防止一直请求
            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
            //缓存消息的数量超过了阈值, 流量控制也是, 这里的意思其实就是说,
            // 这个消费者不是一次两次缓存消息超过数量了,而是超过了1000以上了,就是消费者消费的太慢了,你们发的太快了,打印一把日志
            if ((queueFlowControlTimes++ % 1000) == 0) {
                log.warn(
                    "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
                    this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
            }
            return;
        }
        // 缓存消息大小是否超过100MB,pullThresholdSizeForQueue = 100 ,
        if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
            // 将pullRequest放入队列,只不过是通过后台的定时线程池延迟放入,防止一直请求
            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
            //缓存消息的数量超过了阈值, 流量控制也是, 这里的意思其实就是说,
            // 这个消费者不是一次两次缓存消息大小超过100MB了,而是超过了1000以上了,就是消费者消费的太慢了,你们发的太快了,消息太大了,打印一把日志
            if ((queueFlowControlTimes++ % 1000) == 0) {
                log.warn(
                    "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
                    this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
            }
            return;
        }
        // 消费端是否是排序消费
        if (!this.consumeOrderly) {
            // 判断消息是否跨过了最大的消息跨度 , 通过offset的大小, 缓存的消息 offset的最大跨度不可以超过 consumeConcurrentlyMaxSpan = 2000
            if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
                this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
                if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
                    log.warn(
                        "the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}",
                        processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(),
                        pullRequest, queueMaxSpanFlowControlTimes);
                }
                return;
            }
        } else {
            // 如果是排序消费,那么pullRequest就是需要上锁了,只允许一个人同时操作队列,防止乱序
            if (processQueue.isLocked()) {
                if (!pullRequest.isLockedFirst()) {
                    // 计算顺序消费的话,从哪里开始消费为准, 因为顺序消费的话在下面的
                     // PullCallback onSuccess方法里面,存在一个dispatchToConsume参数
                    // 如果dispatchToConsume = false的话,那么消息就不会被丢到线程池被后台线程消费
                    // ,仅在PullRequest的处理队列里面
                    final long offset = this.rebalanceImpl.computePullFromWhere
                    (pullRequest.getMessageQueue());
                    boolean brokerBusy = offset < pullRequest.getNextOffset();
                    log.info("the first time to pull message, so fix offset from broker. 
                             pullRequest: {} NewOffset: {} brokerBusy: {}",
                        pullRequest, offset, brokerBusy);
                    if (brokerBusy) {
                        log.info("[NOTIFYME]the first time to pull message, but pull request
                        offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
                            pullRequest, offset);
                    }
                    // 设置下次拉取的offSet
                    pullRequest.setLockedFirst(true);
                    pullRequest.setNextOffset(offset);
                }
            } else {
                this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
                log.info("pull message later because not locked in broker, {}", pullRequest);
                return;
            }
        }

        final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().
          get(pullRequest.getMessageQueue().getTopic());
        if (null == subscriptionData) {
            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
            log.warn("find the consumer's subscription failed, {}", pullRequest);
            return;
        }

        final long beginTimestamp = System.currentTimeMillis();
				// 消息结果回调
        PullCallback pullCallback = new PullCallback() {
            @Override
            public void onSuccess(PullResult pullResult) {
                if (pullResult != null) {
                  	// 获取pullResult
                    pullResult = DefaultMQPushConsumerImpl.this.
                      pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
                        subscriptionData);
										// 消息结果状态
                    switch (pullResult.getPullStatus()) {
                        case FOUND: // 找到消息了
                            long prevRequestOffset = pullRequest.getNextOffset();
                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
                            long pullRT = System.currentTimeMillis() - beginTimestamp;
                            DefaultMQPushConsumerImpl.this.getConsumerStatsManager().
                              incPullRT(pullRequest.getConsumerGroup(),
                                pullRequest.getMessageQueue().getTopic(), pullRT);

                            long firstMsgOffset = Long.MAX_VALUE;
                            // 判断消息结果是否为空,
                            if (pullResult.getMsgFoundList() == null || pullResult.getMsgFou
                            ndList().isEmpty()) {
                                // 为空就等待下次继续拉取
                    						DefaultMQPushConsumerImpl.this.
                    												executePullRequestImmediately(pullRequest);
                            } else {
                                // 不为空,获取第一条消息的firstMsgOffset
                                firstMsgOffset = pullResult.getMsgFoundList().get(0).
                                getQueueOffset();

                                // 这里貌似是添加消息的统计信息, TPS之类的
                                DefaultMQPushConsumerImpl.this.getConsumerStatsManager().
                                  incPullTPS(pullRequest.getConsumerGroup(), pullRequest.
                                    getMessageQueue().getTopic(), pullResult.getMsgFoundList()
                                    .size());
                                // 先将本次拉取到的消息放入到pullRequest处理队列里面去
                                boolean dispatchToConsume = processQueue.putMessage(pullResult.
                                getMsgFoundList());

                                // 来来来,哥几个,这里就是重点了,将消息放入后台消费者的线程池并行消费。
                                // dispatchToConsume 这个参数仅针对顺序消费的场景,并行消费的场景不用这个参数
                                DefaultMQPushConsumerImpl.this.consumeMessageService.
                                submitConsumeRequest(
                                    pullResult.getMsgFoundList(),
                                    processQueue,
                                    pullRequest.getMessageQueue(),
                                    dispatchToConsume);

                                // 下次获取消息的时间间隔,pullInterval = 0 ,默认值
                                if (DefaultMQPushConsumerImpl.this.
                                defaultMQPushConsumer.getPullInterval() > 0) {
                                    // 如果大于0 ,则延迟获取
                                    DefaultMQPushConsumerImpl.this.
                                    executePullRequestLater(pullRequest,
                                        DefaultMQPushConsumerImpl.this.
                                        defaultMQPushConsumer.getPullInterval());
                                } else {
                                    // 直接塞到阻塞队列里面去,几乎马上就会继续请求
                                    DefaultMQPushConsumerImpl.this.
                                    executePullRequestImmediately(pullRequest);
                                }
                            }

                            if (pullResult.getNextBeginOffset() < prevRequestOffset
                                || firstMsgOffset < prevRequestOffset) {
                                log.warn(
                                    "[BUG] pull message result maybe 
                                    data wrong, nextBeginOffset: {} 
                                    firstMsgOffset: {} prevRequestOffset: {}",
                                    pullResult.getNextBeginOffset(),
                                    firstMsgOffset,
                                    prevRequestOffset);
                            }

                            break;
                        case NO_NEW_MSG: // 没有新消息
                            // 没有新消息,将pullRequest塞到阻塞队列里面去,几乎马上就会继续请求
                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());

                            DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);

                            DefaultMQPushConsumerImpl.this.
                            executePullRequestImmediately(pullRequest);
                            break;
                        case NO_MATCHED_MSG: // 没有找到匹配的消息
                            // 没有新消息,将pullRequest塞到阻塞队列里面去,几乎马上就会继续请求
                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());

                            DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);

                            DefaultMQPushConsumerImpl.this.
                            executePullRequestImmediately(pullRequest);
                            break;
                        case OFFSET_ILLEGAL:
                            // offset错误,
                            log.warn("the pull request offset illegal, {} {}",
                                pullRequest.toString(), pullResult.toString());
                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
                            // pullRequest设置为丢弃,后面不获取消息了
                            pullRequest.getProcessQueue().setDropped(true);
                            // 延迟执行,10秒钟
                            DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {

                                @Override
                                public void run() {
                                    try {
                                        DefaultMQPushConsumerImpl.this.offsetStore.
                                        updateOffset(pullRequest.getMessageQueue(),
                                            pullRequest.getNextOffset(), false);

                                        DefaultMQPushConsumerImpl.this.offsetStore.
                                        persist(pullRequest.getMessageQueue());
                                        // 移除
                                        DefaultMQPushConsumerImpl.this.rebalanceImpl.
                                        removeProcessQueue(pullRequest.getMessageQueue());

                                        log.warn("fix the pull request offset, {}", pullRequest);
                                    } catch (Throwable e) {
                                        log.error("executeTaskLater Exception", e);
                                    }
                                }
                            }, 10000);
                            break;
                        default:
                            break;
                    }
                }
            }

            @Override
            public void onException(Throwable e) {
                // 如果出现异常的话,走这个地方
                if (!pullRequest.getMessageQueue().getTopic().
                startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    log.warn("execute the pull request exception", e);
                }
                // 将消息请求再次放入队列,只不过放入的是需要延迟一会发送的
                DefaultMQPushConsumerImpl.this.
                executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
            }
        };
        // 获取已提交offSet值
        boolean commitOffsetEnable = false;
        long commitOffsetValue = 0L;
        // 判断是否为集群消费,因为广播消费的offSet不提交到broker上面去, 这个是为了去borker上面获取消息用的
        if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) {
            // 从消息存储器里面获取
            commitOffsetValue = this.offsetStore.
            readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);
            if (commitOffsetValue > 0) {
                commitOffsetEnable = true;
            }
        }

        String subExpression = null;
        boolean classFilter = false;
        // 从负载均衡器里面获取消费组的信息
        SubscriptionData sd = this.rebalanceImpl.getSubscriptionInner().
        get(pullRequest.getMessageQueue().getTopic());
        if (sd != null) {
            if (this.defaultMQPushConsumer.isPostSubscriptionWhenPull()
            && !sd.isClassFilterMode()) {
                subExpression = sd.getSubString();
            }

            classFilter = sd.isClassFilterMode();
        }
        // 构建系统flag
        int sysFlag = PullSysFlag.buildSysFlag(
            commitOffsetEnable, // commitOffset
            true, // suspend
            subExpression != null, // subscription
            classFilter // class filter
        );
        try {
            // 拉取消息
            this.pullAPIWrapper.pullKernelImpl(
                pullRequest.getMessageQueue(),
                subExpression,
                subscriptionData.getExpressionType(),
                subscriptionData.getSubVersion(),
                // 需要消费的offSet起始值
                pullRequest.getNextOffset(),
                // 一次获取消息多少的大小
                this.defaultMQPushConsumer.getPullBatchSize(),
                sysFlag,
                commitOffsetValue,
                // broker端阻塞的时间15秒,默认
                BROKER_SUSPEND_MAX_TIME_MILLIS,
                //过期时间30秒,默认
                CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
                // 默认为异步 , 获取结果后回调pullCallback的onSuccess方法
                CommunicationMode.ASYNC,
                pullCallback
            );
        } catch (Exception e) {
            log.error("pullKernelImpl exception", e);
            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
        }
    }

上面一大坨的代码,有些朋友基本上就慌了,可能瞬间就下关掉这篇文章了,其实上面的代码我的注释基本上写的非常清楚了,下面用文字总结一下

步骤说明:

  1. pullRequest中获取处理队列,每个RocketMq的queue被消费者消费的时候,都会指定一个pullRequest ,
    那么这个queue拉取到的缓存消息,消息统计等信息,都在ProcessQueue里面被包含着。

  2. 设置pullRequest处理队列最后一次的处理时间

  3. 检查服务的状态是否OK, 也就是是否在Running状态

  4. rocketMq的流量控制,push模式下的,总共分为三种流控模式, 在讲流控模式之前,首先我们要知道一点,所有的消息获取过来的时候都是先放到ProcessQueue 里面去的,也就是说ProcessQueue 缓存了所有未处理的消息。

    1. 缓存消息数量控制 ,pullThresholdForQueue = 1000 , 缓存在消费端的消息最多为1000条,如果大于,那么这次就放弃拉取,等待下次
    2. 缓存消息大小控制,缓存消息大小是否超过100MB,pullThresholdSizeForQueue = 100 ,如果大于100MB,那么这次就放弃拉取,等待下次拉取
    3. 针对并行消费(普通消息), 需要控制队列的offSet的跨度, 跨度不可以超过2000,
  5. 针对集群消费,获取已提交offSet值,因为广播消费的offSet不提交到broker上面去, 这个是为了去borker上面获取消息用的

  6. 获取消息,调用pullAPIWrapper.pullKernelImpl 方法,该方法有几个比较重要的参数,

    // 拉取消息
    this.pullAPIWrapper.pullKernelImpl(
      		// 请求的队列
          pullRequest.getMessageQueue(),
      		// 订阅组的信息(className)
          subExpression,
      		// 表达式类型:TAG类型
          subscriptionData.getExpressionType(),
      		//订阅组的版本(时间戳)
          subscriptionData.getSubVersion(),
          // 需要消费的offSet起始值
          pullRequest.getNextOffset(),
          // 一次获取消息多少的大小
          this.defaultMQPushConsumer.getPullBatchSize(),
          sysFlag,
      		// 已经提交的offSet值
          commitOffsetValue,
          // broker端阻塞的时间15秒,默认
          BROKER_SUSPEND_MAX_TIME_MILLIS,
          //过期时间30秒,默认
          CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
          // 默认为异步 , 获取结果后回调pullCallback的onSuccess方法
          CommunicationMode.ASYNC,
          pullCallback
    );
    

    BROKER_SUSPEND_MAX_TIME_MILLIS : 15秒,这个其实就是push模式的实现,当请求到达borker的时候,如果发现没有消息,会阻塞15秒钟,由这个参数控制,

    pullCallback : 获取到结果之后,会进行结果回调,调用onSuccess方法

  7. pullCallback 的结果回调 , 结果回调状态总共有下面几种

    public enum PullStatus {
        /**
         * 消息找到了
         */
        FOUND,
        /**
         * 没有新消息过来
         */
        NO_NEW_MSG,
        /**
         * 没有匹配到的消息
         */
        NO_MATCHED_MSG,
        /**
         * 错误的offSet值,太大了或者太小了
         */
        OFFSET_ILLEGAL
    }
    

    我们主要讲消息找到了的那种场景。

  8. FOUND消息状态

    switch (pullResult.getPullStatus()) {
          case FOUND: // 找到消息了
            long prevRequestOffset = pullRequest.getNextOffset();
            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
            long pullRT = System.currentTimeMillis() - beginTimestamp;
            DefaultMQPushConsumerImpl.this.getConsumerStatsManager().
              incPullRT(pullRequest.getConsumerGroup(),
                        pullRequest.getMessageQueue().getTopic(), pullRT);
    
            long firstMsgOffset = Long.MAX_VALUE;
            // 判断消息结果是否为空,
            if (pullResult.getMsgFoundList() == null||pullResult.getMsgFoundList().isEmpty()) {
              // 为空就等待下次继续拉取
              DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
            } else {
              // 不为空,获取第一条消息的firstMsgOffset
              firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
    
              // 这里貌似是添加消息的统计信息, TPS之类的
              DefaultMQPushConsumerImpl.this.getConsumerStatsManager().
                incPullTPS(pullRequest.getConsumerGroup(),
                           pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
              // 先将本次拉取到的消息放入到pullRequest处理队列里面去
              boolean dispatchToConsume = processQueue.putMessage(pullResult.
                                                                  getMsgFoundList());
              DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
                pullResult.getMsgFoundList(),
                processQueue,
                pullRequest.getMessageQueue(),
                dispatchToConsume);
    
              // 下次获取消息的时间间隔,pullInterval = 0 ,默认值
              if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval()>0) {
                // 如果大于0 ,则延迟获取
                DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
    									DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
              } else {
                   // 直接塞到阻塞队列里面去,几乎马上就会继续请求
                   DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                 }
               }
    
               if (pullResult.getNextBeginOffset() < prevRequestOffset
                   || firstMsgOffset < prevRequestOffset) {
                 log.warn(
                   "[BUG] pull message result maybe data wrong
                   , nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
                   pullResult.getNextBeginOffset(),
                   firstMsgOffset,
                   prevRequestOffset);
               }
    
         break;
       default:
       break;
    }
    
    1.判断消息结果是否为空,为空就等待下次继续拉取,不为空,则继续处理消息
    2.调用`processQueue.putMessage` 方法,先将本次拉取到的消息放入到pullRequest处理队列里面去,`processQueue`维护了一个TreeMap , key = offSet , value为消息。 
    3.putMessage方法中,会维护两个变量来针对顺序消费, `consuming` : 是否在被消费,`dispatchToConsume` : 是否需要分发给消费者消息处理现场
       
      // 顺序消费
      // 线路1:  第一次来了  10 条消息, 设置 dispatchToConsume = true , consuming = true
      //          dispatchToConsume = true , 那么放入一个请求到线程池里面,
      // 线路2:   又来了10 条消息 , 发现consuming = true, 正在消费,则返回dispatchToConsume = false
      // 线路2:   线路2的10条消息不会往线程池里面丢request
    
      // 线路1:  开始读取processQueue里面的消息,
    
      // 线路3:   又来了10 条消息 , 发现consuming = true, 正在消费,则返回dispatchToConsume = false
      // 线路3:   线路3的10条消息不会往线程池里面丢request
    
      // 线路1: 读取完毕线程回归,设置consuming = false
    
      // 线路4:   又来了10 条消息 , 发现consuming = false, 没有在消费,则返回dispatchToConsume = true
      // 线路4:   线路4的会往线程池里面创建一个Request
    
      // 综上所述: 顺序消费有且仅会创建一个线程 。 从而保证顺序消费。通过pullRequest 里面的ProcessQueue 的treeMap ,实现offset最小的先消费
    
    4.将消息submit到线程池里面去,至此消息拉取完毕
    

    总结:
    RocketMq的推模式不是严格意义上的推,是通过后台启动异步线程,一个queue构建一个pullRequest, 异步的去请求的拉模式,只不过是通过broker端阻塞(默认阻塞15秒)的方法,达到了推模式的效果。 其实就是长轮询模式,哈哈

    同时,rocketMq通过流量控制模块(消息数量(1000),消息大小(100MB),offSet跨度(并行消费))来控制消费者这一边的压力,不至于消费慢,被后台异步线程给压死。

sharedCode源码交流群,欢迎喜欢阅读源码的朋友加群,添加下面的微信, 备注”加群“ 。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值