1. 使用demo
以下demo来源于RocketMQ源码中example包
- producer
producer端除了在构建消息时设置tag外,使用putUserProperty方法对消息设置一些属性
public class SqlFilterProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC"};
for (int i = 0; i < 10; i++) {
Message msg = new Message("SqlFilterTest",
tags[i % tags.length],
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
msg.putUserProperty("a", String.valueOf(i));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
producer.shutdown();
}
}
- consumer
consumer端使用MessageSelector.bySql通过sql筛选消息
public class SqlFilterConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
// Don't forget to set enablePropertyFilter=true in broker
consumer.subscribe("SqlFilterTest",
MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
"and (a is not null and a between 0 and 3)"));
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
2. broker端配置方法
在broker的配置文件中添加以下配置项并重启broker:
- enablePropertyFilter = true (启用基于标准的SQL92模式过滤消息)
3. 实现原理
- consumer
(1)调用subscribe(final String topic, final MessageSelector messageSelector)订阅topic并指定SQL表达式。该方法首先判断messageSelector是否为空,如果为空则调用subscribe(String topic, String subExpression)也就是基于tag模式,需要注意的是订阅的是所有的tag;如果不为空则调用build方法根据consumer订阅的topic、表达式以及类型来构造订阅信息,并将订阅信息放入rebalanceImpl的subscriptionInner属性中。
public void subscribe(final String topic, final MessageSelector messageSelector) throws MQClientException {
try {
if (messageSelector == null) {
subscribe(topic, SubscriptionData.SUB_ALL);
return;
}
SubscriptionData subscriptionData = FilterAPI.build(topic,
messageSelector.getExpression(), messageSelector.getExpressionType());
this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData);
if (this.mQClientFactory != null) {
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
}
} catch (Exception e) {
throw new MQClientException("subscription exception", e);
}
}
(2)在发送完心跳信息后会将过滤的信息存储在broker端ROCKETMQ_HOME/store/config/consumerFilter.json文件中
(3)唤醒RebalanceService服务,该服务会构建PullRequest并将其放到PullMessageService的pullRequestQueue中,接着PullMessageService的pullMessage方法回将PullRequest请求发送给broker,在pullMessage方法中会设置subExpression、classFilter消息过滤相关的属性
(4)consumer收到broker端获取到信息后,在processPullResult不会对比消息的tag与rebalanceImpl中获取到的订阅信息中的tag是否一致,因为在consumer端构建订阅信息时不会构建tagSet和codeSet
public static SubscriptionData build(final String topic, final String subString,
final String type) throws Exception {
if (ExpressionType.TAG.equals(type) || type == null) {
return buildSubscriptionData(null, topic, subString);
}
if (subString == null || subString.length() < 1) {
throw new IllegalArgumentException("Expression can't be null! " + type);
}
SubscriptionData subscriptionData = new SubscriptionData();
subscriptionData.setTopic(topic);
subscriptionData.setSubString(subString);
subscriptionData.setExpressionType(type);
return subscriptionData;
}
- broker
broker端处理拉取消息请求的是PullMessageProcessor的processRequest方法,在处理的过程中会根据请求中的consumerGroup在broker端的consumerTable中获取其ConsumerGroupInfo信息并从ConsumerGroupInfo中获取请求中对应topic的订阅信息;在获取topic的订阅信息后,会从broker端的consumerFilterManager中获取consumer的过滤数据consumerFilterData;然后根据订阅信息构建MessageFilter对象;最后在调用DefaultMessageStore的getMessage方法获取消息时会对消息进行过滤,这里与基于tag消息过滤不同,不是对比tag的hashcode而是先从commitlog中获取消息,然后根据consumerFilterData中的过滤条件与消息进行对比是否满足过滤条件。实现消息对比的方法是isMatchedByCommitLog。
else {
ConsumerGroupInfo consumerGroupInfo =
this.brokerController.getConsumerManager().getConsumerGroupInfo(requestHeader.getConsumerGroup());
if (null == consumerGroupInfo) {
log.warn("the consumer's group info not exist, group: {}", requestHeader.getConsumerGroup());
response.setCode(ResponseCode.SUBSCRIPTION_NOT_EXIST);
response.setRemark("the consumer's group info not exist" + FAQUrl.suggestTodo(FAQUrl.SAME_GROUP_DIFFERENT_TOPIC));
return response;
}
if (!subscriptionGroupConfig.isConsumeBroadcastEnable()
&& consumerGroupInfo.getMessageModel() == MessageModel.BROADCASTING) {
response.setCode(ResponseCode.NO_PERMISSION);
response.setRemark("the consumer group[" + requestHeader.getConsumerGroup() + "] can not consume by broadcast way");
return response;
}
subscriptionData = consumerGroupInfo.findSubscriptionData(requestHeader.getTopic());
if (null == subscriptionData) {
log.warn("the consumer's subscription not exist, group: {}, topic:{}", requestHeader.getConsumerGroup(), requestHeader.getTopic());
response.setCode(ResponseCode.SUBSCRIPTION_NOT_EXIST);
response.setRemark("the consumer's subscription not exist" + FAQUrl.suggestTodo(FAQUrl.SAME_GROUP_DIFFERENT_TOPIC));
return response;
}
if (subscriptionData.getSubVersion() < requestHeader.getSubVersion()) {
log.warn("The broker's subscription is not latest, group: {} {}", requestHeader.getConsumerGroup(),
subscriptionData.getSubString());
response.setCode(ResponseCode.SUBSCRIPTION_NOT_LATEST);
response.setRemark("the consumer's subscription not latest");
return response;
}
if (!ExpressionType.isTagType(subscriptionData.getExpressionType())) {
consumerFilterData = this.brokerController.getConsumerFilterManager().get(requestHeader.getTopic(),
requestHeader.getConsumerGroup());
if (consumerFilterData == null) {
response.setCode(ResponseCode.FILTER_DATA_NOT_EXIST);
response.setRemark("The broker's consumer filter data is not exist!Your expression may be wrong!");
return response;
}
if (consumerFilterData.getClientVersion() < requestHeader.getSubVersion()) {
log.warn("The broker's consumer filter data is not latest, group: {}, topic: {}, serverV: {}, clientV: {}",
requestHeader.getConsumerGroup(), requestHeader.getTopic(), consumerFilterData.getClientVersion(), requestHeader.getSubVersion());
response.setCode(ResponseCode.FILTER_DATA_NOT_LATEST);
response.setRemark("the consumer's consumer filter data not latest");
return response;
}
}
}
if (!ExpressionType.isTagType(subscriptionData.getExpressionType())
&& !this.brokerController.getBrokerConfig().isEnablePropertyFilter()) {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("The broker does not support consumer to filter message by " + subscriptionData.getExpressionType());
return response;
}
MessageFilter messageFilter;
if (this.brokerController.getBrokerConfig().isFilterSupportRetry()) {
messageFilter = new ExpressionForRetryMessageFilter(subscriptionData, consumerFilterData,
this.brokerController.getConsumerFilterManager());
} else {
messageFilter = new ExpressionMessageFilter(subscriptionData, consumerFilterData,
this.brokerController.getConsumerFilterManager());
}
decodeProperties方法会解析从commitlog中获取到的消息,evaluate方法实现消息属性对比。
public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map<String, String> properties) {
if (subscriptionData == null) {
return true;
}
if (subscriptionData.isClassFilterMode()) {
return true;
}
if (ExpressionType.isTagType(subscriptionData.getExpressionType())) {
return true;
}
ConsumerFilterData realFilterData = this.consumerFilterData;
Map<String, String> tempProperties = properties;
// no expression
if (realFilterData == null || realFilterData.getExpression() == null
|| realFilterData.getCompiledExpression() == null) {
return true;
}
if (tempProperties == null && msgBuffer != null) {
tempProperties = MessageDecoder.decodeProperties(msgBuffer);
}
Object ret = null;
try {
MessageEvaluationContext context = new MessageEvaluationContext(tempProperties);
ret = realFilterData.getCompiledExpression().evaluate(context);
} catch (Throwable e) {
log.error("Message Filter error, " + realFilterData + ", " + tempProperties, e);
}
log.debug("Pull eval result: {}, {}, {}", ret, realFilterData, tempProperties);
if (ret == null || !(ret instanceof Boolean)) {
return false;
}
return (Boolean) ret;
}
4. SQL92基本语法
RocketMQ只定义了一些基本语法来支持这个特性。
- 数值比较,比如:>,>=,<,<=,BETWEEN,=;
- 字符比较,比如:=,<>,IN;
- IS NULL 或者 IS NOT NULL;
- 逻辑符号 AND,OR,NOT;
常量支持类型为:
- 数值,比如:123,3.1415;
- 字符,比如:‘abc’,必须用单引号包裹起来;
- NULL,特殊的常量
- 布尔值,TRUE 或 FALSE
使用push模式的消费者和DefaultLitePullConsumer可以用使用SQL92标准的sql语句,接口如下:
public void subscribe(final String topic, final MessageSelector messageSelector)
5.比较两种消息过滤方式
- 两种方式在实现上整体流程都是一样的
- 基于tag方式在订阅topic时需要指定tag,多个tag之间使用||间隔;基于SQL92方式在订阅topic时需要提供SQL表达式
- 基于tag方式在broker仅仅会比较tag的hashcode,由于存在hash值冲突,所以会在consumer进行二次比较,二次比较的是tag值;基于SQL92方式在broker端会从commitlog中获取消息并根据SQL表达式进行对比;
- 使用SQL92方式进行消息过滤需要在broker添加“enablePropertyFilter=true”配置项并重启broker