RocketMQ(五):揭秘高吞吐量并发消费原理
上篇文章已经描述过拉取消息的流程,消息被拉取到消费者内存后就会提交消费消息请求从而开始消费消息
消费消息分为两种方式:并发消费、顺序消费
- 并发消费采用多线程进行消费,能够大大提升消费吞吐量,但无法保证消费顺序
- 顺序消费会通过加锁的方式进行有序消费,但吞吐量、性能不如并发
本篇文章就来聊聊并发消费,揭秘RocketMQ高吞吐量并发消费的原理
思维导图如下:
往期好文:
RocketMQ(一):消息中间件缘起,一览整体架构及核心组件
RocketMQ(二):揭秘发送消息核心原理(源码与设计思想解析)
RocketMQ(三):面对高并发请求,如何高效持久化消息?(核心存储文件、持久化核心原理、源码解析)
消费者消费流程
ConsumeMessageConcurrentlyService 消费消息
上篇文章说到,拉取完消息后会提交消费请求,便于后续进行异步消费
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest
submitConsumeRequest
方法有并发、顺序两种实现,先来查看并发实现
ConsumeMessageConcurrentlyService.submitConsumeRequest
并发的实现主要会根据每次批量消费的最大数量进行构建请求并提交,如果期间失败会延时5s再提交
public void submitConsumeRequest(
final List<MessageExt> msgs,
final ProcessQueue processQueue,
final MessageQueue messageQueue,
final boolean dispatchToConsume) {
//每次批量消费最大数量 默认为1 (消息充足的情况下,默认每次拉取32条消息)
final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
//如果拉取到的消息小于等于每次批量消费数量 则构建请求并提交
if (msgs.size() <= consumeBatchSize) {
ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);
try {
this.consumeExecutor.submit(consumeRequest);
} catch (RejectedExecutionException e) {
//失败 延迟5s再提交请求
this.submitConsumeRequestLater(consumeRequest);
}
} else {
//如果拉取到的消息 大于 每次批量消费数量,则分批次构建请求并提交
for (int total = 0; total < msgs.size(); ) {
List<MessageExt> msgThis = new ArrayList<MessageExt>(consumeBatchSize);
for (int i = 0; i < consumeBatchSize; i++, total++) {
if (total < msgs.size()) {
msgThis.add(msgs.get(total));
} else {
break;
}
}
ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue);
try {
this.consumeExecutor.submit(consumeRequest);
} catch (RejectedExecutionException e) {
for (; total < msgs.size(); total++) {
msgThis.add(msgs.get(total));
}
this.submitConsumeRequestLater(consumeRequest);
}
}
}
}
并发、顺序实现中,构建的请求ConsumeRequest
并不是共享的,而是它们的内部类,虽然同名但是实现是不同的
提交后使用消费线程池执行,在执行任务的过程中,主要会调用消费监听器进行消费消息 consumeMessage
,然后通过成功/失败的情况进行处理结果processConsumeResult
(在此期间还会封装上下文,执行消费前、后的钩子方法,记录消费耗时等操作)
public void run() {
//检查processQueue
if (this.processQueue.isDropped()) {
log.info("the message queue not be able to consume, because it's dropped. group={} {}", ConsumeMessageConcurrentlyService.this.consumerGroup, this.messageQueue);
return;
}
//获取并发消息监听器 (我们设置消费者时实现的)
MessageListenerConcurrently listener = ConsumeMessageConcurrentlyService.this.messageListener;
//上下文 用于执行消费过程中的钩子方法
ConsumeConcurrentlyContext context = new ConsumeConcurrentlyContext(messageQueue);
//状态
ConsumeConcurrentlyStatus status = null;
//失败重试的情况下会更新topic
defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());
//保留上下文信息 执行消费前的钩子方法时使用
ConsumeMessageContext consumeMessageContext = null;
if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) {
consumeMessageContext = new ConsumeMessageContext();
consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace());
consumeMessageContext.setConsumerGroup(defaultMQPushConsumer.getConsumerGroup());
consumeMessageContext.setProps(new HashMap<String, String>());
consumeMessageContext.setMq(messageQueue);
consumeMessageContext.setMsgList(msgs);
consumeMessageContext.setSuccess(false);
ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext);
}
//计时
long beginTimestamp = System.currentTimeMillis();
//记录是否异常
boolean hasException = false;
ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
try {
//每个消息记录开始时间
if (msgs != null && !msgs.isEmpty()) {
for (MessageExt msg : msgs) {
MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis()));
}
}
//消费消息
status = listener.consumeMessage(Collections.unmodifiableList(msgs), context);
} catch (Throwable e) {
hasException = true;
}
//消费耗时
long consumeRT = System.currentTimeMillis() - beginTimestamp;
//计算返回类型
if (null == status) {
if (hasException) {
returnType = ConsumeReturnType.EXCEPTION;