前言
推消息的常用示例
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,带着这个问题,笔者调试了一波代码,一顿操作猛如虎,总算搞了个头绪出来。
- 程序启动的时候, 首先往这个队列里面放元素的是负载均衡器,
org.apache.rocketmq.client.impl.consumer.RebalanceImpl
,负载均衡器会根据负载均衡算法计算出当前消费者需要消费的队列, 每一个队列构建一个pullRequest
, 然后放入队列
- 每次请求消息之后,会把
pullRequest
重新放入队列, 以供接下来继续请求 - 程序在运行过程中,负载均衡器检测到有新增加的队列,那么会把新增加的队列作为一个
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);
}
}
步骤说明:
- 从MQClientFactory里面获取消费者信息, 这个在
DefaultMQPushConsumer
初始化的时候,注册进去的。 当时注册的是DefaultMQPushConsumerImpl
- 强转为
DefaultMQPushConsumerImpl
, 如果不是的话,那么肯定就报错了,所以这里做了一个限制 - 调用
DefaultMQPushConsumerImpl
的pullMessage
方法
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);
}
}
上面一大坨的代码,有些朋友基本上就慌了,可能瞬间就下关掉这篇文章了,其实上面的代码我的注释基本上写的非常清楚了,下面用文字总结一下
步骤说明:
-
从
pullRequest
中获取处理队列,每个RocketMq的queue被消费者消费的时候,都会指定一个pullRequest
,
那么这个queue拉取到的缓存消息,消息统计等信息,都在ProcessQueue
里面被包含着。 -
设置pullRequest处理队列最后一次的处理时间
-
检查服务的状态是否OK, 也就是是否在Running状态
-
rocketMq的流量控制,push模式下的,总共分为三种流控模式, 在讲流控模式之前,首先我们要知道一点,所有的消息获取过来的时候都是先放到
ProcessQueue
里面去的,也就是说ProcessQueue
缓存了所有未处理的消息。- 缓存消息数量控制 ,
pullThresholdForQueue = 1000
, 缓存在消费端的消息最多为1000条,如果大于,那么这次就放弃拉取,等待下次 - 缓存消息大小控制,缓存消息大小是否超过100MB,
pullThresholdSizeForQueue = 100
,如果大于100MB,那么这次就放弃拉取,等待下次拉取 - 针对并行消费(普通消息), 需要控制队列的offSet的跨度, 跨度不可以超过2000,
- 缓存消息数量控制 ,
-
针对集群消费,获取已提交offSet值,因为广播消费的offSet不提交到broker上面去, 这个是为了去borker上面获取消息用的
-
获取消息,调用
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方法 -
pullCallback
的结果回调 , 结果回调状态总共有下面几种public enum PullStatus { /** * 消息找到了 */ FOUND, /** * 没有新消息过来 */ NO_NEW_MSG, /** * 没有匹配到的消息 */ NO_MATCHED_MSG, /** * 错误的offSet值,太大了或者太小了 */ OFFSET_ILLEGAL }
我们主要讲消息找到了的那种场景。
-
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跨度(并行消费))来控制消费者这一边的压力,不至于消费慢,被后台异步线程给压死。