RocketMQ 消息过滤流程

本文基于RocketMQ 4.2.0版本进行源码分析,讲述了其消息过滤流程。介绍了三种消息过滤类型,包括标签匹配、SQL匹配和自定义匹配,并详细阐述了各自背后的机制与实现原理,如标签匹配的过滤机制、SQL匹配的编译和存储过程、自定义匹配的过滤服务器和过滤类使用等。

基于 RocketMQ 4.2.0 版本进行的源码分析。
讲述 RocketMQ 消息过滤流程

一、消息过滤类型

Producer 在发送消息的时候可以指定消息的标签类型,还可以为每一个消息添加一个或者多个额外的属性:

// 指定标签
Message msg = new Message("TopicTest", "TagA", ("Hello RocketMQ").getBytes(RemotingHelper.DEFAULT_CHARSET));
// 添加属性 a
msg.putUserProperty("a", 5);

根据标签和属性的不同,RocketMQ 客户端在消费消息的时候有三种消息过滤类型:

(1) 标签匹配

consumer.subscribe(“TopicTest”, “TagA | TagB | TagC”);

(2) SQL 匹配

consumer.subscribe("TopicTest",
                MessageSelector.bySql(
                    "(TAGS is not null and TAGS in ('TagA', 'TagB'))" + 
                "and (a is not null and a between 0  3)"));

(3) 自定义匹配

客户端实现 MessageFilter 类,自定义过滤逻辑:

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
File classFile = new File(classLoader.getResource("MessageFilterImpl.java").getFile());

String filterCode = MixAll.file2String(classFile);
consumer.subscribe("TopicTest", "org.apache.rocketmq.example.filter.MessageFilterImpl",filterCode);

对于 MessageFilter 类实现 match 方法即可:

public class MessageFilterImpl implements MessageFilter {

    @Override
    public boolean match(MessageExt msg, FilterContext context) {
        String property = msg.getProperty("SequenceId");
        if (property != null) {
            int id = Integer.parseInt(property);
            if (((id % 10) == 0) &&
                (id > 100)) {
                return true;
            }
        }

        return false;
    }
    
}

下面我们一一讲解各自背后的机制与实现原理。

二、标签匹配

当为消息指定消息标签类型的时候,实际上所指定的标签例如 TagA 是作为一个属性放入到了这条消息中的:

public class Message implements Serializable {

    public void setTags(String tags) {
        this.putProperty(MessageConst.PROPERTY_TAGS, tags);
    }
    
}

当这条消息到达 Broker 服务器端后,用户设置的标签会计算为标签码,默认的计算方式采用的标签字符串的 hashCode() 作为计算结果的:

public class CommitLog {

    public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer,
                                                     final boolean checkCRC,
                                                     final boolean readBody) {
        // ...
        String tags = propertiesMap.get(MessageConst.PROPERTY_TAGS);
        if (tags != null && tags.length() > 0) {
            tagsCode = MessageExtBrokerInner
                .tagsString2tagsCode(MessageExt.parseTopicFilterType(sysFlag), tags);
        }
        // ...
    }
    
}

当计算出来标签码之后,这条消息的标签码会被存放至消费队列文件中,用来与消费者客户端消费队列的标签码进行匹配。消费者客户端订阅消费话题的时候,会指定想要匹配的标签类型:
consumer.subscribe(“TopicTest”, “TagA | TagB | TagC”);
这段代码在内部实现中利用 FilterAPI 构建了一个 SubscriptionData 对象:

public class DefaultMQPushConsumerImpl implements MQConsumerInner {

    public void subscribe(String topic, String subExpression) throws MQClientException {
        SubscriptionData subscriptionData = FilterAPI
            .buildSubscriptionData(this.defaultMQPushConsumer.getConsumerGroup(),
                                   topic,
                                   subExpression);
        // ...
    }
    
}

当用户未指定标签或者指定为星号标签的时候,则代表用户接受所有标签的消息。如果用户指定了一个或者多个标签,那么会将每一个标签取其 hashCode() 放入到 codeSet中。SubscriptionData还有一个 expressionType 字段,在使用标签匹配的时候,其不会设置这个这个字段的值,因此其保留为 null。在这些信息设置好以后,当客户端发送心跳包的时候,会将这些话题的注册信息一并上传至 Broker 服务器端,方便在 Broker 端进行匹配。

public class SubscriptionData implements Comparable<SubscriptionData> {

    public final static String SUB_ALL = "*";

    private Set<String> tagsSet = new HashSet<String>();
    private Set<Integer> codeSet = new HashSet<Integer>();

    private String expressionType;
    
}

当 Broker 端服务器在取消息的时候,每取出来一条消息,都会执行两道过滤机制:

  • ConsumeQueue 文件匹配
  • CommitLog 文件匹配

任一检查没有通过后,绝不会放行这条消息给客户端:

public class DefaultMessageStore implements MessageStore {

    public GetMessageResult getMessage(final String group, /** 其他参数 **/) {

        for (; i < bufferConsumeQueue.getSize() && i < maxFilterMessageCount; i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {

            // ConsumeQueue 文件匹配
            if (messageFilter != null
                && !messageFilter.isMatchedByConsumeQueue(isTagsCodeLegal ? tagsCode : null, extRet ? cqExtUnit : null)) {
                if (getResult.getBufferTotalSize() == 0) {
                    status = GetMessageStatus.NO_MATCHED_MESSAGE;
                }

                continue;
            }

            // CommitLog 文件匹配
            if (messageFilter != null
                && !messageFilter.isMatchedByCommitLog(selectResult.getByteBuffer().slice(), null)) {
                if (getResult.getBufferTotalSize() == 0) {
                    status = GetMessageStatus.NO_MATCHED_MESSAGE;
                }
                // release...
                selectResult.release();
                continue;
            }

        }
        
    }
    
}

消息过滤器的默认实现是 ExpressionMessageFilter ,消息过滤的默认实现策略就是看这个话题的标签码集合中是否包括当前这条消息的标签码:

public class ExpressionMessageFilter implements MessageFilter {

    @Override
    public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) {
        // ...
        if (ExpressionType.isTagType(subscriptionData.getExpressionType())) {

            if (tagsCode == null) {
                return true;
            }

            if (subscriptionData.getSubString().equals(SubscriptionData.SUB_ALL)) {
                re
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值