如题所示,本文围绕顺序消息,延时消息与消息过滤来展开。

一,顺序消息

RocketMQ只能保证队列级别的消息有序,如果要实现某一类消息的顺序执行,就必须将这类消息发送到同一个队列,可以在消息发送时使用 MessageQueueSelector,通过指定sharding key进而将同一类消息发送到同一队列里,这样在CommitLog文件里消息的顺序就与发送时一致了。broker端选择发送的队列可以参考之前是的文章:RocketMQ学习五-选择队列等特性

下面再说下消费端的处理。

顺序消息消费的事件监听器是MessageListenerOrderly。我们知道PullMessageService根据偏移量拉取一批消息后会存入ProcessQueue中,然后使用线程池进行处理。要保证消费端对单队列中的消息顺序处理,故在多线程场景下需要按照消息消费队列进行加锁。顺序消费在消费端的并发度并不取决消费端线程池的大小,而是取决于分给给消费者的队列数量,故如果一个 Topic 是用在顺序消费场景中,建议消费者的队列数设置增多,可以适当为非顺序消费的 2~3 倍,这样有利于提高消费端的并发度,方便横向扩容。

消费端的横向扩容或 Broker 端队列个数的变更都会触发消息消费队列的重新负载,并发消费时一个消费队列有可能被多个消费者同时消费,但顺序消费时并不会出现这种情况,因为顺序消息不仅仅在消费消息时会锁定消息消费队列,在分配到消息队列时,能从该队列拉取消息还需要在 Broker 端申请该消费队列的锁,即同一个时间只有一个消费者能拉取该队列中的消息,确保顺序消费的语义。

image.png
流程:

  1. PullMessageService单线程的从Broker获取消息
  2. PullMessageService将消息添加到ProcessQueue中(ProcessMessage是一个消息的缓存),之后提交一个消费任务到ConsumeMessageOrderService
  3. ConsumeMessageOrderService多线程执行,每个线程在消费消息时需要拿到MessageQueue的锁
  4. 拿到锁之后从ProcessQueue中获取消息

那如果顺序消费的过程中消费失败了怎么处理呢?并发消费模式在消费失败是有重试机制,默认重试 16 次,而且重试时是先将消息发送到 Broker,然后再次拉取到消息。而对于顺序消息来说这种机制就会丧失其消费的顺序性。还有如果一条消息一直不能消费成功会一直重试(准确来说是Integer.MAX_VALUE次)直到消费成功,一直失败会导致消息其消息消费进度会无法向前推进,即会造成消息积压现象,所以顺序消费时我们一定要捕捉异常。

二,延时消息

在开源版本的RocketMQ中延时消息并不支持任意时间的延时,目前默认设置为:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,从1s到2h分别对应着等级1到18,而阿里云中的付费版本是可以支持40天内的任何时刻(毫秒级别)。

延时消息的流程图:
image.png

  1. Producer在自己发送的消息上设置好需要延时的级别(比如设置3等级的延迟:message.setDelayTimeLevel(3))。
  2. Broker发现此消息是延时消息(消息的 delayLevel 大于0),将Topic进行替换成延时Topic(SCHEDULE_TOPIC_XXXX),每个延时级别都会作为一个单独的queue(delayLevel-1),将自己的Topic作为额外信息存储(CommitLog#putMessage方法里)。
  3. 构建ConsumerQueue
  4. 定时任务定时每隔1s扫描每个延时级别的ConsumerQueue。
  5. 拿到ConsumerQueue中的CommitLog的Offset,获取消息,判断是否已经达到执行时间
  6. 如果达到,那么将消息的Topic恢复,进行重新投递。如果没有达到则延迟没有达到的这段时间执行任务。

在延时消息这一块内容里再提一下消费失败的情形。

消费者消费一条消息后,需要回复broker消息的消费状态,消费状态共有两种,consume_success表示消费成功,reconsume_later表示稍后重新消费,在实现消费逻辑时,如果消费失败,并希望可以重新消费,重新消费又是如何实现的呢?
答案就是失败的消息会被放入到延时队列(topic为SCHEDULE_TOPIC_XXXX,且每消费失败一次,level会加1)里去。broker在启动的时候会开启ScheduleMessageService定时任务,它的功能是用于处理延时队列中的消息,每个队列都有一个专门的Timer定时器去轮询其中的消息,拉到消息后如果发现时间已到则将其存入CommitLog里,这样消费者就可以进行消费了;如果时间未到则忽略。如果超过了最大重试次数则会进入死信队列。
详情可以参考RocketMq消费失败处理逻辑

三,消息过滤

RocketMQ支持SQL过滤与TAG过滤两种方式。

  • SQL过滤:在broker端进行,可以减少无用数据的网络传输但broker压力会大,性能低,支持使用SQL语句复杂的过滤逻辑。
  • TAG过滤:在broker与consumer端进行,增加无用数据的网络传输但broker压力小,性能高,只支持简单的过滤。

SQL过滤先不分析了,可以参考文章:RocketMQ源码解析:消息过滤是如何实现的?
TAG过滤的流程大概是,broker获取对应ConsuemrQueue里hashcode(tag)后根据消费端传入的tag进行比较,如果不匹配则将此消息跳过;如果匹配消费端还要进行一次tag的比较,因为会有可能出现了hash冲突。

broker端的过滤:

    //查询消息入口
    public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset,
        final int maxMsgNums,
        final MessageFilter messageFilter) {
 
        //tag过滤,在consumerQueue里
       if (messageFilter != null
            && !messageFilter.isMatchedByConsumeQueue(isTagsCodeLegal ? tagsCode : null, extRet ? cqExtUnit : null)) {
             if (getResult.getBufferTotalSize() == 0) {
                  status = GetMessageStatus.NO_MATCHED_MESSAGE;
             }
                continue;
        }
    //tag过滤,在commitlog里
    if (messageFilter != null
        && !messageFilter.isMatchedByCommitLog(selectResult.getByteBuffer().slice(), null)) {
        if (getResult.getBufferTotalSize() == 0) {
             status = GetMessageStatus.NO_MATCHED_MESSAGE;
         }
         // release...
         selectResult.release();
         continue;
    }
}

consumer过滤:

public PullResult processPullResult(final MessageQueue mq, final PullResult pullResult,
        final SubscriptionData subscriptionData) {
        PullResultExt pullResultExt = (PullResultExt) pullResult;

        this.updatePullFromWhichNode(mq, pullResultExt.getSuggestWhichBrokerId());
        if (PullStatus.FOUND == pullResult.getPullStatus()) {
            ByteBuffer byteBuffer = ByteBuffer.wrap(pullResultExt.getMessageBinary());
            List<MessageExt> msgList = MessageDecoder.decodes(byteBuffer);

            List<MessageExt> msgListFilterAgain = msgList;
            if (!subscriptionData.getTagsSet().isEmpty() && !subscriptionData.isClassFilterMode()) {
                msgListFilterAgain = new ArrayList<MessageExt>(msgList.size());
                for (MessageExt msg : msgList) {
                    if (msg.getTags() != null) {
                        if (subscriptionData.getTagsSet().contains(msg.getTags())) {
                            msgListFilterAgain.add(msg);
                        }
                    }
                }
            }

            if (this.hasHook()) {
                FilterMessageContext filterMessageContext = new FilterMessageContext();
                filterMessageContext.setUnitMode(unitMode);
                filterMessageContext.setMsgList(msgListFilterAgain);
                this.executeHook(filterMessageContext);
            }

            ......

        }

        pullResultExt.setMessageBinary(null);

        return pullResult;
    }

消息过滤还可以通过topic来实现,我们在使用topic进行过滤还是使用tag过滤可以根据具体的业务场景进行选择,一般来说,不同的 Topic 之间的消息没有必然的联系,而 Tag 则用来区分同一个 Topic 下相互关联的消息。

参考文章
顺序消息参考:13 结合实际场景顺序消费、消息过滤实战
聊一聊顺序消息(RocketMQ顺序消息的实现机制)
延时消息参考:rocketmq一个topic多个group_你需要知道的RocketMQ
消息过滤参考:rocketMQ消息Tag过滤原理


步履不停
38 声望13 粉丝

好走的都是下坡路