RocketMQ消息过滤实现原理

消费者订阅了某个topic主题后,RocketMQ会将该主题中的所有消息投递给消费者。若消费者只需要关注部分消息,可通过设置订阅消息过滤条件在RocketMQ服务端进行过滤,只获取到需要关注的消息子集,避免接收到大量无效的消息。

1445a94d97994b5db52f0c2c197e51c0.png       


订阅过滤消息

发送消息

这里再次给出发送消息的代码块,主要是订阅过滤消息时,要与发送的消息Tag、消息属性关联起来使用。

// 实例化一个生产者来产生消息
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroup");
 // 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
// 启动生产者
producer.start();
// 设置topic:TRADE, 消息体为:Hello message
Message message = new Message("TRADE", ("Hello message").getBytes()); 
// 设置TAG (主要用于后续订阅消息的Tag或SQL92过滤)
message.setTags("TAG1");
// 设置消息属性 (主要用于后续订阅消息的SQL92过滤)
message.putUserProperty("amount, "30");
// 发送消息 
producer.send(message);  
// 关闭生产者
producer.shutdown()

订阅过滤消息

消费者消费消息时,需要设置对某类消息的订阅,RocketMQ推荐使用的订阅方法如下:

/**
 * 此方法默认是按Tag类型及其表达式进行消息过滤
*/
void subscribe(final String topic, final String subExpression) throws MQClientException;

/**
 * 此方法支持按Tag或SQL92类型进行消息过滤
*/
void subscribe(final String topic, final MessageSelector selector) throws MQClientException;

MessageSelector类是构建消息过滤,类只有两个属性:

public class MessageSelector {
 // 目前只有SQL92 与Tag
 private String type;
 // 表达式内容,如:(a > 10 AND a < 100) OR (b IS NOT NULL AND b=TRUE)
 private String expression;    
}

可以看出rocketmq目前支持的消息过滤是通过Tag或SQL92这两种类型及其相关对应的表达式进行消息订阅过滤。


发送订阅的心跳信息

消费者端在启动时,会执行subscribe方法订阅消息的逻辑:

  1. 根据订阅的类型与表达式构建订阅数据(SubscriptionData);

  2. 把订阅数据放到RebalanceImpl中的缓存Map中({key:topic,value: SubscriptionData}),后续当消费者做负载均衡时调用使用;

  3. 当前实例加锁后,发送心跳把订阅信息给所有的broker服务器。

    1. 若是按Tag过滤,broker则把订阅信息通过ConsumerManager#registerConsumer进行注册,存到 ConsumerManager中的ConcurrentMap<String/* Group */, ConsumerGroupInfo> consumerTable 缓存中;

    2. 若是按SQL92过滤,不仅执行a的操作,还会通过ConsumerFilterManager#register进行注册,存到ConcurrentMap<String/*Topic*/,FilterDataMapByTopic>  filterDataByTopic缓存中,FilterDataMapByTopic的结构包含一个topic属性和一个缓存ConcurrentMap<String/*consumer group*/,ConsumerFilterData> groupFilterData字段,ConsumerFilterData结构包含经过编译后的sql表达式compiledExpression(SelectorParser#parse,by JavaCC)、BloomFilterData数据(主要是bitPos数组,bitNum),是通过消费组名+“#”+topic拼接成字符串后创建生产的。


消息过滤逻辑

消息过滤逻辑实现是通过MessageFilter 接口,此接口中定义了两个方法,具体如下所示:

public interface MessageFilter {
    /**
     * 按消费队列过滤
     * @param tagsCode tag的hashcode
     * @param cqExtUnit 消费队列的扩展属性
     */
    boolean isMatchedByConsumeQueue(final Long tagsCode, final ConsumeQueueExt.CqExtUnit cqExtUnit);

    /**
     * 按消息属性通过commit log.
     * @param msgBuffer 存储时commitLog的完整消息
     * @param properties 消息属性
     */
    boolean isMatchedByCommitLog(final ByteBuffer msgBuffer, final Map<String, String> properties);
}

21f4c2e6e29c42f88dddec3e396759ef.png

接口的实现如上所示,Tag与SQL92过滤实现的逻辑都在 ExpressionMessageFilter 中,ExpressionForRetryMessageFilter为支持重试Topic的 Filter 实现。


Tag类型过滤实现

Tag过滤是通过isMatchedByConsumeQueue方法,broker端执行消息过滤步骤:

  1. 先判断消息的tagscode是否为空

  2. 判断订阅表达式是否等于 “ * ”;

  3. 判断tags哈希值缓存Set中是否包含消息的tagsCode值。

 public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) {
        if (null == subscriptionData) {
            return true;
        }

        if (subscriptionData.isClassFilterMode()) {
            return true;
        }

        // by tags code.
        if (ExpressionType.isTagType(subscriptionData.getExpressionType())) {

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

            if (subscriptionData.getSubString().equals(SubscriptionData.SUB_ALL)) {
                return true;
            }

            return subscriptionData.getCodeSet().contains(tagsCode.intValue());
        } 

        return true;
    }

因为boker端只判断的tags的hashcode值,可能存在不同的tags哈希值相同,在消费者端处理拉取结果的方法 PullApiWrapper#processPullResult 中,再次根据tags的字符串进行精确判断,判断tags字符串值缓存Set中是否包含消息的 tags值,则返回给消费者消费。


SQL92类型过滤实现

调用isMatchedByConsumeQueue,先利用布隆过滤器进行第一层过滤,从ConsumeQueueExt 中取出消息 Reput 时计算的 BitMap,过滤订阅的所有 SQL92 消费者名称,判断拉消息的消费者组是否可能需要消费到这条消息;

public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) {
         // ......
         // no expression or no bloom
            if (consumerFilterData == null || consumerFilterData.getExpression() == null
                || consumerFilterData.getCompiledExpression() == null || consumerFilterData.getBloomFilterData() == null) {
                return true;
            }

            // message is before consumer
            if (cqExtUnit == null || !consumerFilterData.isMsgInLive(cqExtUnit.getMsgStoreTime())) {
                log.debug("Pull matched because not in live: {}, {}", consumerFilterData, cqExtUnit);
                return true;
            }

            byte[] filterBitMap = cqExtUnit.getFilterBitMap();
            BloomFilter bloomFilter = this.consumerFilterManager.getBloomFilter();
            if (filterBitMap == null || !this.bloomDataValid
                || filterBitMap.length * Byte.SIZE != consumerFilterData.getBloomFilterData().getBitNum()) {
                return true;
            }

            BitsArray bitsArray = null;
            try {
                bitsArray = BitsArray.create(filterBitMap);
                boolean ret = bloomFilter.isHit(consumerFilterData.getBloomFilterData(), bitsArray);
                log.debug("Pull {} by bit map:{}, {}, {}", ret, consumerFilterData, bitsArray, cqExtUnit);
                return ret;
            } catch (Throwable e) {
                log.error("bloom filter error, sub=" + subscriptionData
                    + ", filter=" + consumerFilterData + ", bitMap=" + bitsArray, e);
            }
        return true;
    }

调用isMatchedByCommitLog 方法,使用JavaCC编译好的 expression 表达式进行过滤逻辑判断。

ConsumerFilterData realFilterData = this.consumerFilterData;
Map<String, String> tempProperties = properties;
MessageEvaluationContext context = new MessageEvaluationContext(tempProperties);
Object ret = realFilterData.getCompiledExpression().evaluate(context);
public interface Expression {

    /**
     * Calculate express result with context.
     *
     * @param context context of evaluation
     * @return the value of this expression
     */
    Object evaluate(EvaluationContext context) throws Exception;
}

可以看到有19个实现了表达式过滤接口,具体支持的逻辑表达式语法可以看这些实现类代码或RocketMQ的官方文档。

86a6e574b890490e9795760dc33c17f8.png

参考:

消息过滤 | RocketMQ

https://javacc.github.io/javacc/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值