如题所示,本文围绕顺序消息,延时消息与消息过滤来展开。
一,顺序消息
RocketMQ只能保证队列级别的消息有序,如果要实现某一类消息的顺序执行,就必须将这类消息发送到同一个队列,可以在消息发送时使用 MessageQueueSelector,通过指定sharding key进而将同一类消息发送到同一队列里,这样在CommitLog文件里消息的顺序就与发送时一致了。broker端选择发送的队列可以参考之前是的文章:RocketMQ学习五-选择队列等特性。
下面再说下消费端的处理。
顺序消息消费的事件监听器是MessageListenerOrderly。我们知道PullMessageService根据偏移量拉取一批消息后会存入ProcessQueue中,然后使用线程池进行处理。要保证消费端对单队列中的消息顺序处理,故在多线程场景下需要按照消息消费队列进行加锁。顺序消费在消费端的并发度并不取决消费端线程池的大小,而是取决于分给给消费者的队列数量,故如果一个 Topic 是用在顺序消费场景中,建议消费者的队列数设置增多,可以适当为非顺序消费的 2~3 倍,这样有利于提高消费端的并发度,方便横向扩容。
消费端的横向扩容或 Broker 端队列个数的变更都会触发消息消费队列的重新负载,并发消费时一个消费队列有可能被多个消费者同时消费,但顺序消费时并不会出现这种情况,因为顺序消息不仅仅在消费消息时会锁定消息消费队列,在分配到消息队列时,能从该队列拉取消息还需要在 Broker 端申请该消费队列的锁,即同一个时间只有一个消费者能拉取该队列中的消息,确保顺序消费的语义。
流程:
- PullMessageService单线程的从Broker获取消息
- PullMessageService将消息添加到ProcessQueue中(ProcessMessage是一个消息的缓存),之后提交一个消费任务到ConsumeMessageOrderService
- ConsumeMessageOrderService多线程执行,每个线程在消费消息时需要拿到MessageQueue的锁
- 拿到锁之后从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天内的任何时刻(毫秒级别)。
延时消息的流程图:
- Producer在自己发送的消息上设置好需要延时的级别(比如设置3等级的延迟:message.setDelayTimeLevel(3))。
- Broker发现此消息是延时消息(消息的 delayLevel 大于0),将Topic进行替换成延时Topic(SCHEDULE_TOPIC_XXXX),每个延时级别都会作为一个单独的queue(delayLevel-1),将自己的Topic作为额外信息存储(CommitLog#putMessage方法里)。
- 构建ConsumerQueue
- 定时任务定时每隔1s扫描每个延时级别的ConsumerQueue。
- 拿到ConsumerQueue中的CommitLog的Offset,获取消息,判断是否已经达到执行时间
- 如果达到,那么将消息的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过滤原理
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。