如题所示,本文围绕顺序消息,延时消息与消息过滤来展开。
一,顺序消息
RocketMQ只能保证队列级别的消息有序,如果要实现某一类消息的顺序执行,就必须将这类消息发送到同一个队列,可以在消息发送时使用 MessageQueueSelector,通过指定sharding key进而将同一类消息发送到同一队列里,这样在CommitLog文件里消息的顺序就与发送时一致了。broker端选择发送的队列可以参考之前是的文章:RocketMQ学习五-选择队列等特性。
下面再说下消费端的处理。
顺序消息消费的事件监听器是MessageListenerOrderly。我们知道PullMessageService根据偏移量拉取一批消息后会存入ProcessQueue中,然后使用线程池进行处理。要保证消费端对单队列中的消息顺序处理,故在多线程场景下需要按照消息消费队列进行加锁。顺序消费在消费端的并发度并不取决消费端线程池的大小,而是取决于分给给消费者的队列数量,故如果一个 Topic 是用在顺序消费场景中,建议消费者的队列数设置增多,可以适当为非顺序消费的 2~3 倍,这样有利于提高消费端的并发度,方便横向扩容。
消费端的横向扩容或 Broker 端队列个数的变更都会触发消息消费队列的重新负载,并发消费时一个消费队列有可能被多个消费者同时消费,但顺序消费时并不会出现这种情况,因为顺序消息不仅仅在消费消息时会锁定消息消费队列,在分配到消息队列时,能从该队列拉取消息还需要在 Broker 端申请该消费队列的锁,即同一个时间只有一个消费者能拉取该队列中的消息,确保顺序消费的语义。
那如果顺序消费的过程中消费失败了怎么处理呢?并发消费模式在消费失败是有重试机制,默认重试 16 次,而且重试时是先将消息发送到 Broker,然后再次拉取到消息,这种机制就会丧失其消费的顺序性。还有如果一条消息如果一直不能消费成功,其消息消费进度就会一直无法向前推进,即会造成消息积压现象,所以顺序消费时我们一定要捕捉异常。
二,延时消息
在开源版本的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恢复,进行重新投递。如果没有达到则延迟没有达到的这段时间执行任务。
三,消息过滤
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 msgList = MessageDecoder.decodes(byteBuffer);
List msgListFilterAgain = msgList;
if (!subscriptionData.getTagsSet().isEmpty() && !subscriptionData.isClassFilterMode()) {
msgListFilterAgain = new ArrayList(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一个topic多个group_你需要知道的RocketMQ
消息过滤参考:rocketMQ消息Tag过滤原理