RocketMQ分布式消息队列的消息过滤方式有别于其它MQ中间件,是在Consumer端订阅消息时再做消息过滤的。
RocketMQ这么做是在于其Producer端写入消息和Consumer端订阅消息采用分离存储的机制来实现的,Consumer端订阅消息是需要通过ConsumeQueue这个消息消费的逻辑队列拿到一个索引,然后再从CommitLog里面读取真正的消息实体内容。
流程:生产者向broker写入消息,先将消息写入到commitlog文件中,然后异步的创建每一个MessageQueue对应的ConsumerQueue,消费者在消费消息时,先找ConsumerQueue,拿到消息的元数据,元数据里有tag的哈希值,判断哈希值是否一样,如果一样,就根据偏移量去commit log中获取消息的内容,不一样丢弃。
consumeQueue位于/root/store,其ConsumeQueue的存储结构如下,存储有消息的偏移量、消息的大小和tag的哈希值,可以看到其中有8个字节存储的Message Tag的哈希值,基于Tag的消息过滤正式基于这个字段值的。
,目录如下:
2种过滤方式
(1) Tag过滤方式:
Consumer端在订阅消息时除了指定Topic还可以指定TAG,如果一个消息有多个TAG,可以用||分隔。
1. Consumer端会将这个订阅请求构建成一个 SubscriptionData,发送一个Pull消息的请求给Broker端。
2. Broker端从RocketMQ的文件存储层—Store读取数据之前,会用这些数据先构建一个MessageFilter,然后传给Store。
3. Store从 ConsumeQueue读取到一条记录后,会用它记录的消息tag hash值去做过滤。
4. 在服务端只是根据hashcode进行判断,无法精确对tag原始字符串进行过滤,在消息消费端拉取到消息后,还需要对消息的原始tag字符串进行比对,如果不同,则丢弃该消息,不进行消息消费。
public class MyProducer1 {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("producer_grp_06");
producer.setNamesrvAddr("node1:9876");
producer.start();
Message message = null;
for (int i = 0; i < 100; i++) {
message = new Message(
"tp_demo_06",
"tag-" + (i % 3),
("hello lagou - " + i).getBytes()
);
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult.getSendStatus());
}
@Override
public void onException(Throwable e) {
System.out.println(e.getMessage());
}
});
}
Thread.sleep(3_000);
producer.shutdown();
}
}
public class MyConsumerTag1 {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_grp_06_03");
consumer.setNamesrvAddr("node1:9876");
// consumer.subscribe("tp_demo_06", "*");
// consumer.subscribe("tp_demo_06", "tag-1");
consumer.subscribe("tp_demo_06", "tag-1||tag-0");
consumer.setMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
final MessageQueue messageQueue = context.getMessageQueue();
final String brokerName = messageQueue.getBrokerName();
final String topic = messageQueue.getTopic();
final int queueId = messageQueue.getQueueId();
System.out.println(brokerName + "\t" + topic + "\t" + queueId);
for (MessageExt msg : msgs) {
System.out.println(msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 初始化并启动消费者
consumer.start();
}
}
(2) SQL92的过滤方式:
仅对push的消费者起作用。
Tag方式虽然效率高,但是支持的过滤逻辑比较简单。
SQL表达式可以更加灵活的支持复杂过滤逻辑,这种方式的大致做法和上面的Tag过滤方式一样,只是在Store层的具体过滤过程不太一样,真正的 SQL expression 的构建和执行由rocketmq-filter模块负责的。
每次过滤都去执行SQL表达式会影响效率,所以RocketMQ使用了BloomFilter避免了每次都去执行。
SQL92的表达式上下文为消息的属性。
开启支持SQL92的特性,修改配置文件conf/broker.conf
RocketMQ仅定义了几种基本的语法,用户可以扩展:
1. 数字比较: >, >=, <, <=, BETWEEN, =
2. 字符串比较: =, <>, IN; IS NULL或者IS NOT NULL;
3. 逻辑比较: AND, OR, NOT;
4. Constant types are: 数字如:123, 3.1415; 字符串如:'abc',必须是单引号引起来 NULL,特殊常量 布尔型如:TRUE or FALSE;
消费者:
for (int i = 0; i < 100; i++) {
message = new Message( "tp_demo_06_02", ("hello lagou - " + i).getBytes() );
String value = null;
switch (i % 3) {
case 0:
value = "v0";
break;
case 1:
value = "v1";
break;
default:
value = "v2";
break;
}
// 给消息添加用户属性
message.putUserProperty("mykey", value);
消费者:
consumer.setNamesrvAddr("node1:9876");
// consumer.subscribe("tp_demo_06_02", MessageSelector.bySql("mykey in ('v0', 'v1')"));
// consumer.subscribe("tp_demo_06_02", MessageSelector.bySql("mykey = 'v0'"));
consumer.subscribe("tp_demo_06_02", MessageSelector.bySql("mykey IS NOT NULL"));
(3) Filter Server方式。
这是一种比SQL表达式更灵活的过滤方式,允许用户自定义Java函数,根据Java函数的逻辑对消息进行过滤。要使用Filter Server,首先要在启动Broker前在配置文件里加上filterServer-Nums=3 这样的配置,Broker在启动的时候,就会在本机启动3个Filter Server进程。Filter Server类似一个RocketMQ的Consumer进程,它从本机Broker获取消息,然后根据用户上传过来的Java函数进行过滤,过滤后的消息再传给远端的Consumer。这种方式会占用很多Broker机器的CPU资源,要根据实际情况谨慎使用。上传的java代码也要经过检查,不能有申请大内存、创建线程等这样的操作,否则容易造成Broker服务器宕机。