함께 쓰는 습관을 들이세요! "너겟 데일리 뉴플랜·4월 업데이트 챌린지" 참여 15일차입니다. 클릭하시면 이벤트 내용을 보실 수 있습니다 .
개요
RocketMQ의 소비자가 소비 메시지를 얻는 방법에는 푸시 모드와 풀 모드의 두 가지가 있습니다.
- 푸시 모드: 서버가 메시지를 클라이언트에 푸시
- 풀 모드: 클라이언트가 서버에 계속 요청(아마도 메시지)
사실, 이 두 가지 모드의 기본 구현은 소비자가 능동적으로 메시지를 가져오는 방식입니다.
Push 방식은 Consumer Polling 과정을 캡슐화하여 MessageListener 리스너를 등록하고, 메시지를 pull 하면 리스너가 깨어나 소비를 하기 때문에 서버에서 메시지를 푸시한 것 같은 느낌을 줍니다.
끌어오기 모드에서 소비자가 메시지를 가져오는 프로세스를 작성해야 합니다.
Topic
모든 MQ 얻기 에 따르면- MQ에서 메시지 가져오기
- 나중에 이 위치에서 메시지를 계속 받을 수 있도록 메시지를 가져오려는 다음 시간의 오프셋을 기록합니다.
실시간 메시지를 보장하는 RocketMQ의 메커니즘은 무엇입니까? 여기서 RocketMQ는 동기 IO의 개념과 약간 유사한 긴 연결 방식을 사용하는데, 서버에서 메시지가 없으면 데이터나 타임아웃이 있을 때까지 요청을 차단하고 반환하지 않습니다. Consumer는 메시지를 처리한 후 위의 과정을 반복하면서 새로운 요청을 서버에 보낸다...
푸시 메시지 소비 프로세스
전체 코드 실행 흐름은 다음과 같습니다.
메시지 소비 활성화
DefaultMQPushConsumerImpl#start
메서드의 섹션으로 돌아가기 :
// 注册消费者
boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
// 注册失败则抛出异常
if (!registerOK) {
this.serviceState = ServiceState.CREATE_JUST;
this.consumeMessageService.shutdown(defaultMQPushConsumer.getAwaitTerminationMillisWhenShutdown());
throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
+ "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
null);
}
// 开启消息消费
mQClientFactory.start();
复制代码
MQ 소비자 등록이 완료된 후 mQClientFactory.start();
메시지를 소비하기 위해 소비자를 시작하기 위해 호출되는 것을 볼 수 있으며 소스 코드는 다음과 같다.
public void start() throws MQClientException {
synchronized (this) {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
// 如果注册中心得url未给出,可以通过Http请求从其他地方获取
if (null == this.clientConfig.getNamesrvAddr()) {
this.mQClientAPIImpl.fetchNameServerAddr();
}
// 启动响应-响应通道
this.mQClientAPIImpl.start();
// 启动多个定时任务
this.startScheduledTask();
// 启动pull取消息服务
this.pullMessageService.start();
// Start rebalance service
this.rebalanceService.start();
// 传入false表示不启动push服务
this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
log.info("the client factory [{}] start OK", this.clientId);
this.serviceState = ServiceState.RUNNING;
break;
case START_FAILED:
throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
default:
break;
}
}
}
复制代码
메시지 소비가 활성화된 후 서비스의 상태에 따라 메시지 소비를 위한 다양한 준비가 수행됨을 소스 코드에서 알 수 있습니다.
- 서비스 상태 가 인 경우
CREATE_JUST
먼저 서비스 상태를 로 설정하고START_FAILED
모든 초기화 작업이 정상적으로 완료되면 서비스 상태가 로 설정되어RUNNING
실행 중임을 나타냅니다. this.mQClientAPIImpl.start();
Open request-response, 하단 레이어는 Netty 기반this.startScheduledTask();
启动多个定时任务,如果此时注册中心地址为null
,则会没2min获取一次this.pullMessageService.start();
开始拉取消息this.rebalanceService.start();
消费者开启负载均衡消费消息,每20s一次,根据负载均衡策略选择机器
接收消息
通过上面的分析,this.pullMessageService.start();
开启了接收消息模式,其中PullMessageService
类继承了ServiceThread
类,意味着PullMessageService
使用多线程消费消息。 当
PullMessageService
对象调用start
方法时,会执行多线程定义的run
方法来拉取消息:
@Override
public void run() {
log.info(this.getServiceName() + " service started");
// 服务没有停止则执行循环
while (!this.isStopped()) {
try {
// 使用BlockingQueue阻塞队列,获取队列中的请求并执行
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");
}
复制代码
其中用到了BlockingQueue
阻塞队列来进行消息的拉取,当提交了消息拉取请求后,如果队列里面为空则立刻执行。
可以发现DefaultMQPushConsumerImpl
调用的还是拉取消息的方法,拉取消息的源码如下:
public void pullMessage(final PullRequest pullRequest) {
/*
......
*/
// 判断队列是否被lock住,如果被锁住了,则延迟一段时间再拉取
// 顺序消费的逻辑
if (processQueue.isLocked()) {
if (!pullRequest.isPreviouslyLocked()) {
long offset = -1L;
try {
offset = this.rebalanceImpl.computePullFromWhereWithException(pullRequest.getMessageQueue());
} catch (Exception e) {
this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
log.error("Failed to compute pull offset, pullResult: {}", pullRequest, e);
return;
}
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);
}
pullRequest.setPreviouslyLocked(true);
pullRequest.setNextOffset(offset);
}
} else {
this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
log.info("pull message later because not locked in broker, {}", pullRequest);
return;
}
// 获取Topic对应的订阅信息,如果不存在,则延迟拉取消息
final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
if (null == subscriptionData) {
this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
log.warn("find the consumer's subscription failed, {}", pullRequest);
return;
}
/*
......
*/
}
复制代码
拉取消息的具体处理步骤如下:
- 判断
ProcessQueue
是否被废弃,如果为true
则直接返回 - 记录最后拉取消息的时间
- 判断Consumer是否正在运行,如果返回
false
则延迟3000ms拉取消息 - 判断Consumer是否被锁住,如果返回
true
则延迟1000ms拉取消息 - 判断Consumer持有的消息数量是否超过最大数量1000,如果返回
true
则说明消费者缓冲区已经满了,延迟50ms拉取消息 - 判断消息
Offset
是否大于2000,如果返回true
则延迟50ms拉取消息 - 顺序消费消息,使用分布式锁锁定MQ来保证一条一条消费消息。如果MQ不是被第一次锁定,则从上一次消费到的位置开始消费,如果获取锁失败则延迟一段时间再拉取消息
- 如果获取到的
offset
小于nextOffset
,说明已经越界,延迟3000ms再进行消费
默认消费方式是从MaxOffset开始往前消费的
- 获取
Topic
对应的订阅消息如果不存在则延迟拉3000ms取消息 - 拉取消息使用带有回调的
PullCallback
,当拉取消息成功时开始消费
具体的消费消息的逻辑是使用向pullCallback
中传入匿名内部类的方式,一旦拉取到消息之后,就会回调到内部类中的onSuccess
方法执行具体的逻辑,消费消息的具体流程源码如下所示:
PullCallback pullCallback = new PullCallback() {
@Override
public void onSuccess(PullResult pullResult) {
// 拉取结果不为空
if (pullResult != null) {
pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
subscriptionData);
switch (pullResult.getPullStatus()) {
case FOUND:
// 设置下次拉取消息的offset
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 = pullResult.getMsgFoundList().get(0).getQueueOffset();
DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
// 提交拉取到的消息到processQueue中,返回上一批次的消息是否已经消费完了
boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
// 在有序模式下,只有dispatchToConsume为true才提交,并发模式不受影响
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
pullResult.getMsgFoundList(),
processQueue,
pullRequest.getMessageQueue(),
dispatchToConsume);
// 如果处理消息都需要和上次保持一定时间间隔,则稍后再执行
if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
} else {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
}
// 当前offset小于上一次的offset则报错
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:
case NO_MATCHED_MSG: // 没有匹配到消息的情况
// 设置下次拉取消息的offset
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());
// 设置下次拉取消息的offset
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
// 设置ProcessQueue废弃
pullRequest.getProcessQueue().setDropped(true);
// 提交延迟处理任务,将ProcessQueue移除
DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
@Override
public void run() {
try {
// 更新消费进度,同步消费进度到Broker
DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
pullRequest.getNextOffset(), false);
DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());
// 将ProcessQueue移除
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, pullTimeDelayMillsWhenException);
}
复制代码
根据上面的代码,具体的消费消息的流程:
- 使用
pullAPIWrapper
获取内存中ByteBuffer
中的数据,得到拉取消息的结果 - 根据拉取消息后结果的状态来判断是否有消息可以被消费
a. FOUND
:发现消息 b. NO_NEW_MSG
:没有新消息可被拉取 c. NO_MATCHED_MSG
:消息不匹配 d. OFFSET_ILLEGAL
:offset非法,可能是消息太大
如果是FOUND
状态,表示可以进行下一步的消费,之后的步骤如下:
- 获取消息拉取的offset,设置下一次拉取消息的offset,同时统计消息消费响应时间
- 如果没有拉取到消息,马上进行下一次拉取,如果拉取到消息,则把消息提交至
ProcessQueue
里 - 否则,提交拉取到的消息到processQueue中,返回上一批次的消息是否已经消费完了(在有序模式下,只有dispatchToConsume为true才提交,并发模式不受影响);如果处理消息都需要和上次保持一定时间间隔,则稍后再执行
如果是OFFSET_ILLEGAL
状态,之后的步骤如下:
- 设置下次拉取消息的offset
- 设置ProcessQueue废弃
- 提交延迟处理任务,更新消费进度,同步消费进度到Broker,并将ProcessQueue移除
ProcessQueue
기본 데이터 구조는 검색 효율성을 개선하여 메시지가 사용TreeMap
되었는지 여부를 신속하게 결정하는 데 사용 됩니다.