消费者订阅了某个topic主题后,RocketMQ会将该主题中的所有消息投递给消费者。若消费者只需要关注部分消息,可通过设置订阅消息过滤条件在RocketMQ服务端进行过滤,只获取到需要关注的消息子集,避免接收到大量无效的消息。
订阅过滤消息
发送消息
这里再次给出发送消息的代码块,主要是订阅过滤消息时,要与发送的消息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方法订阅消息的逻辑:
-
根据订阅的类型与表达式构建订阅数据(SubscriptionData);
-
把订阅数据放到RebalanceImpl中的缓存Map中({key:topic,value: SubscriptionData}),后续当消费者做负载均衡时调用使用;
-
当前实例加锁后,发送心跳把订阅信息给所有的broker服务器。
-
若是按Tag过滤,broker则把订阅信息通过ConsumerManager#registerConsumer进行注册,存到 ConsumerManager中的ConcurrentMap<String/* Group */, ConsumerGroupInfo> consumerTable 缓存中;
-
若是按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);
}
接口的实现如上所示,Tag与SQL92过滤实现的逻辑都在 ExpressionMessageFilter 中,ExpressionForRetryMessageFilter为支持重试Topic的 Filter 实现。
Tag类型过滤实现
Tag过滤是通过isMatchedByConsumeQueue方法,broker端执行消息过滤步骤:
-
先判断消息的tagscode是否为空
-
判断订阅表达式是否等于 “ * ”;
-
判断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的官方文档。
参考: