消息消费过程

1.消息消费过程

PullMessageService 负责对消息队列进行消息拉取,从远端服务器拉取消息后将消息存入 ProcessQueue 消息队列处理队列中,然后调用 Consum Message Ser

vice#submitConsumeRequest 方法进行消息消费,使用线程池来消费消息,确保了消息拉取与消息消费的解祸

1.1消息消费

消费者消息消费服务 ConsumeMessageConcurrentlyService 的主要方法是 submitConsumeRequest提交消费请求,具体逻辑如下

if (msgs.size() <= consumeBatchSize) {
    ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);
    try {
        this.consumeExecutor.submit(consumeRequest);
    } catch (RejectedExecutionException e) {
        this.submitConsumeRequestLater(consumeRequest);
    }
}

consumeMessageBatchMaxSize ,消息批次,在这里看来也就是一次消息消费任务 CosumeRequest 中包含的消息条数,默认为1,默认为 msgs.size ()默认最多为 32 条,受DefaultMQPushConsumer.pullBatchSize 属性控制,如果 ms gs.size ()小于 cons Message

atchMaxSize 直接将拉取到的消息放入到ConsumeRequest中,然后将consumeRequest提交到消息消费者线程池中,如果提交过程中出现拒绝提交异常则延迟 5s 再提交,这里其实是给出一种标准的拒绝提交实现方式,实际过程中由于消费者线程池使用的任务队列为

LinkedBlockingQueue 无界队列,故不会出现拒绝提交异常

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);
    }
}

如果拉取的消息条数大于 consumeMessageBatchMaxSize 则对拉取消息进行分页,每页 consumeMessagBatchMaxSize 条消息,创建多个 ConsumeRequest 务并提交到消费线程池.ConsumRequest的 run()法封装了具体消息消费逻辑

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;
}

进入具体消息消费时会先检查 processQueue的dropped ,如果设置为 true,则停止消费该队列的消费,在进行消息重新负载时如果该消息队列被分配给消费组内其他消费者后,需要 droped 设置为 true 阻止消费者继续消费不属于自己的消息队列

public void resetRetryAndNamespace(final List<MessageExt> msgs, String consumerGroup) {
    final String groupTopic = MixAll.getRetryTopic(consumerGroup);
    for (MessageExt msg : msgs) {
        String retryTopic = msg.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);
        if (retryTopic != null && groupTopic.equals(msg.getTopic())) {
            msg.setTopic(retryTopic);
        }

        if (StringUtils.isNotEmpty(this.defaultMQPushConsumer.getNamespace())) {
            msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace()));
        }
    }
}

恢复重试消息主题名 。这是为什么呢?这是由消息重试机制决定的, RocketMQ将消息存入 commitlog 文件时,如果发现消息的延时级别 delayTimeLevel 会首先将重试主题存人在消息的属性中,然后设置主题名称为 SCHEDULE TOPIC ,以便时间到后重新参与消息消费

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) {
    log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}",
        RemotingHelper.exceptionSimpleDesc(e),
        ConsumeMessageConcurrentlyService.this.consumerGroup,
        msgs,
        messageQueue);
    hasException = true;
}

执行具体的消息消费,调用应用程序消息监昕器的 consumeMessage 方法,进入到具体的消息消费业务逻辑,返回该批消息的消费结果 最终将返回 CONSUME SUCCESS (消费成功)或 RECONSUME LATER (需要重新消费)

if (!processQueue.isDropped()) {
    ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this);
} else {
    log.warn("processQueue is dropped without process consume result. messageQueue={}, msgs={}", messageQueue, msgs);
}

执行业务消息消费后,在处理结果前再次验证 ProcessQueue的isDroped状态值,如果设置为 true,将不对结果进行处理,也就是说如果在消息消费过程中进入到这里 时,如果由于由新的消费者加入或原先的消费者出现若机导致原先分给消费者的队列在负载之后分配给别的消费者,那么在应用程序的角度来看的话,消息会被重复消费

switch (this.defaultMQPushConsumer.getMessageModel()) {
            case BROADCASTING:
                for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
                    MessageExt msg = consumeRequest.getMsgs().get(i);
                    log.warn("BROADCASTING, the message consume failed, drop it, {}", msg.toString());
                }
                break;
            case CLUSTERING:
                List<MessageExt> msgBackFailed = new ArrayList<MessageExt>(consumeRequest.getMsgs().size());
                for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
                    MessageExt msg = consumeRequest.getMsgs().get(i);
                    boolean result = this.sendMessageBack(msg, context);
                    if (!result) {
                        msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
                        msgBackFailed.add(msg);
                    }
                }

                if (!msgBackFailed.isEmpty()) {
                    consumeRequest.getMsgs().removeAll(msgBackFailed);

                    this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
                }
                break;
            default:
                break;
        }

如果是广播模式, 业务方返回 RECONSUME_LATER ,消息并不会重新被消费,只是以警告级别输出到日志文件 如果是集群模式, 消息消费成功,由于 acklndex =consumeRequest.getMsgs().size()-1,故 i=acklndex+1 等于 consumeRequest.getMsgs().size()

并不会执行 sendMessageBack 只有在业务方返回 RECONSUME_LATER 时,该批消息都需要发 ACK 消息,如果消息发送 ACK 失败,则直接将本批 ACK 消费发送失败的消息再次封装为 ConsumeRequest ,然后延迟 5s 重新消费 ,如果 ACK 消息发送成功,则该消息

会延迟消费

long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());
if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
    this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
}

从 Process Queue 移除这 消息, 这里返回的偏移量是移除该批消息后最小的偏移量,然后用该偏移量更新消息 费进度 ,以便在消费者重启后能从上一次的消费

进度开始消费,避免消息重复消费 值得重点注意的是当消息监听器返回 RECONSUME

LATER ,消息消费进度也会向前推进,用 Proces sQueue 最小的队列偏移量调用消息消费

进度存储器 OffsetStore 更新消费进度,这是因为当返回 RECONSUME_LATER, RocketMQ

会创建 条与原先消息属性相同的消息,拥有一个唯 的新 msgld ,并存储原消息 ID ,该

消息会存入到 commitlog 文件中,与原先的消息没有任何关联,那该消息当然也会进入到

ConsuemeQueue 队列中,将拥有 个全新的队列偏移

1.2消息确认(ACK)

如果消息监听器返回的消费结果为 RECONSUME_LATER ,则需要将这些消息发送给Broker 延迟消息 ,如果发送 ACK 消息失败,将延迟 5s 后提交线程池进行消费, ACK消息发送的网络客户端人口: MQClientAPIImpl#consumerSendMessageBack ,命令编码:

String newTopic = MixAll.getRetryTopic(requestHeader.getGroup());
int queueIdInt = Math.abs(this.random.nextInt() % 99999999) % subscriptionGroupConfig.getRetryQueueNums();

创建重试主题,重试主题名称 %RETRY%+消费组名称,并从重试队列中随机选择一个队列 ,并构建 TopicConfig 主题配置信息

MessageExt msgExt = this.brokerController.getMessageStore().lookMessageByOffset(requestHeader.getOffset());
if (null == msgExt) {
    response.setCode(ResponseCode.SYSTEM_ERROR);
    response.setRemark("look message by offset failed, " + requestHeader.getOffset());
    return CompletableFuture.completedFuture(response);
}

final String retryTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);
if (null == retryTopic) {
    MessageAccessor.putProperty(msgExt, MessageConst.PROPERTY_RETRY_TOPIC, msgExt.getTopic());
}
msgExt.setWaitStoreMsgOK(false);

根据消息物理偏移量 commitlog 文件中获取消息, 同时将消息的主题存入属性中

设置消息重试次数,如果果消息 重试次数超过 maxReconsumeTimes ,再次改变newTopic 主题为 DLQ (” %DLQ%”),该主题的权限为只写,说明消息一旦进入到 DLQ列中, RocketMQ 将不负责再次调度进行消费了, 需要人工干预

MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
msgInner.setTopic(newTopic);
msgInner.setBody(msgExt.getBody());
msgInner.setFlag(msgExt.getFlag());
MessageAccessor.setProperties(msgInner, msgExt.getProperties());
msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(null, msgExt.getTags()));

msgInner.setQueueId(queueIdInt);
msgInner.setSysFlag(msgExt.getSysFlag());
msgInner.setBornTimestamp(msgExt.getBornTimestamp());
msgInner.setBornHost(msgExt.getBornHost());
msgInner.setStoreHost(msgExt.getStoreHost());
msgInner.setReconsumeTimes(msgExt.getReconsumeTimes() + 1);

String originMsgId = MessageAccessor.getOriginMessageId(msgExt);
MessageAccessor.setOriginMessageId(msgInner, UtilAll.isBlank(originMsgId) ? msgExt.getMsgId() : originMsgId);

根据原先的消息创建一个新的消息对象,重试消息会 拥有自己的唯一消息 ID( ms ld )并存人到 commitlog 中,并不会去更新原先消息, 而是会将原先的主题、 消息id存入消息属性中,主题名称为重试主题,其他属性与原先消息保持相同

将消息存入 CommitLog 件中 ,部分逻辑在前面 节中己 细介绍,这里想再 个机制,消息重试机制依托于定时任务实现

1.3消费进度管理

广播模式:同一个消费组的所有消息消费者都需要消费主题下的所有消息,也就是同组内的消费者的消息消费行为是对立的,互相不影响,故消息进度需要独立存储,最理想的存储地方应该是与消费者绑定

集群模式:同一个消费组内的所有消息消费者共享消息主题下的所有消息, 一条消息(同一个消息消费队列)在同一时间只会被消费组内的一个消费者消费,并且随着消费队列的动态变化重新负载,所以消费进度需要保存在一个每个消费者都能访问到的地方

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页