RocketMQ TAG/SQL过滤消费实现原理以及消息丢失问题

一、背景

在分析完RocketMQ Consumer端的消息拉取流程之后,发现一个很有意思的钩子函数,可以用来自定义消息的过滤消费。那么我们来看一下RocketMQ是如何实现过滤消费的?

二、消息过滤方式

1、使用Tag标签

在大多数情况下,TAG是一个简单而有用的设计,其可以来简单的选择我们想要的消息。

1)实现方式

Consumer代码:

public static void main(String[] args) throws Exception {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("study-consumer");
    consumer.setNamesrvAddr("10.10.100.100:9876");

    // 消费者订阅topic时,添加Tag过滤规则
    consumer.subscribe("saint-study-topic", "TAG-A");

    // 集群模式,只有一个consumer能消费到消息。
    consumer.setMessageModel(MessageModel.CLUSTERING);

    consumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
            for (MessageExt msg : msgs) {
                System.out.println(new String(msg.getBody()));
            }

            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });

    consumer.start();
    System.out.println("Consumer start。。。。。。");

}

生产者代码:

public static void main(String[] args) throws Exception {
    DefaultMQProducer producer = new DefaultMQProducer("saint-study");
    // 设置nameserver地址
    producer.setNamesrvAddr("10.10.100.100:9876");
    producer.start();

    // topic 和body
    Message msg = new Message("saint-study-topic", "TAG-A", "key01", "study010".getBytes(StandardCharsets.UTF_8));
    Message msg2 = new Message("saint-study-topic", "TAG-B", "key02", "study011".getBytes(StandardCharsets.UTF_8));
    Message msg3 = new Message("saint-study-topic", "TAG-C", "key03", "study012".getBytes(StandardCharsets.UTF_8));
    Message msg4 = new Message("saint-study-topic", "TAG-A", "key04", "study013".getBytes(StandardCharsets.UTF_8));

    List<Message> list = new ArrayList<>();
    list.add(msg);
    list.add(msg2);
    list.add(msg3);
    list.add(msg4);

    // 批量消息发送消息
    SendResult send = producer.send(list);
    System.out.println("sendResult: " + send);

    // 关闭生产者
    producer.shutdown();
    System.out.println("已经停机");
}

运行生产者,我们可以发现成功生产了4条消息:
在这里插入图片描述
运行消费者,我们可以发现只消费到了TAG为“TAG-A”的消息,即:study010、study013
在这里插入图片描述

2)错误使用TAG过滤消费导致消息丢失问题!

(1)问题描述
  1. 启动消费者1,消费组为study-consumer,订阅saint-study-topic的消息,tag设置为TAG-A
  2. 启动消费者2,消费组也为study-consumer,也订阅saint-study-topic的消息,但是tag设置为TAG-B
  3. 启动生产者,生产者发送含有TAG-A,TAG-B的消息各3条
  4. 消费者1没有收到任何消息,消费者2收到部分消息(tag为TAG-B)

从下图我们可以看到,消费者2消费到了tag为TAG-B的消息;而消费者1并没有消费到任何消息
在这里插入图片描述
在这里插入图片描述

(2)结论

同一个消费组,给不同的消费者设置不同的tag时,后启动的消费者会覆盖先启动的消费者设置的tag。

  • tag是消息过滤的条件,经过服务端和客户端两层过滤,最后只有后启动的消费者才能收到部分消息。
(3)原因总结
  • 同一个consumer group的订阅关系,保存在RebalanceImpl类的Map中。key为topic;
    在这里插入图片描述
    再看SubscriptionData中,有一个tagsSet用来表示当前topic对应消费组的包含的Tag信息。
    在这里插入图片描述
    那么问题来了:集群模式下的如果多个consumer在一个实例上怎区分?
  • SubscriptionData和topic的对应关系是在RebalanceImpl中记录的,每个DefaultMQPushConsumerImpl都有自己的RebalanceImpl;所以消费端的SubscriptionData信息不会被覆盖。但是Broker端采用一个MAP保存topic和TAG的对应关系,会覆盖TAG信息

每个消费者都有自己的SubscriptionData,我们可以看到消费者1的SubscriptionData中的tagsSet为TAG-A。
在这里插入图片描述
再看从broker端拉到的消息,它属性中的tag为TAG-B。因为consumer2是最后注册的,所以服务端会根据TAG-B做一遍消息过滤再返回。
在这里插入图片描述

  • 不同的消费者启动后,依次向Broker端注册订阅关系,因为tag不一样,导致Broker端Map中同一topic的tag被覆盖。比如:消费者1订阅tag1,消费者2订阅tag2。最后Broker端的map中只保存tag2;
  • 过滤的核心是是tag,tag被更新,过滤条件被改变。服务端过滤后只返回tag2的消息;
  • 客户端接收消息后,再次过滤。先启动的消费者1订阅tagA,但是服务端返回tag2,所以消费者1收不到任何消息。消费者2能收到一半的消息(集群模式,假设消息平均分配,另外一半分给tag2)

2、使用SQL过滤

Tag过滤语法简单,但是其灵活性也比较差,相对比较适合过滤场景简单 且 客户端对计算资源不是很敏感的用户。如果想实现更为复杂的消息过滤功能可以使用Sql Filter。

注意:只有使用push模式的消费者才能用使用SQL92标准的sql语句。

1)基本语法

RocketMQ只定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。

  • 数值比较,比如:>,>=,<,<=,BETWEEN,=;
  • 字符比较,比如:=,<>,IN;
  • IS NULL 或者 IS NOT NULL;
  • 逻辑符号 AND,OR,NOT;

常量支持类型为:

  • 数值,比如:123,3.1415;
  • 字符,比如:‘abc’,必须用单引号包裹起来;
  • NULL,特殊的常量
  • 布尔值,TRUE 或 FALSE

2)实现方式

Producer代码:

public static void main(String[] args) throws Exception {
    DefaultMQProducer producer = new DefaultMQProducer("saint-study");
    // 设置nameserver地址
    producer.setNamesrvAddr("10.10.100.100:9876");
    producer.start();

    // topic 和body
    List<Message> messages = new ArrayList<>();
    for (int i = 10; i < 30; i ++) {
        Message msg = new Message("saint-study-topic", "TAG-B", "key02", ("study000" + i).getBytes(StandardCharsets.UTF_8));
        msg.putUserProperty("age", String.valueOf(i));
        messages.add(msg);
    }

    SendResult send = producer.send(messages);
    System.out.println("sendResult: " + send);

    // 关闭生产者
    producer.shutdown();
    System.out.println("已经停机");
}

Consumer代码:

public static void main(String[] args) throws Exception {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("study-consumer");
    consumer.setNamesrvAddr("10.10.100.100:9876");

    // 给consumer关联SQL过滤规则
    MessageSelector messageSelector = MessageSelector.bySql("age >= 18 and age <= 28");
    consumer.subscribe("saint-study-topic", messageSelector);
    consumer.setMessageModel(MessageModel.CLUSTERING);

    consumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
            for (MessageExt msg : msgs) {
                System.out.println(new String(msg.getBody()));
            }

            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });

    consumer.start();
    System.out.println("Consumer start。。。。。。");

}

三、原理剖析

1、TAG过滤原理

1)服务端过滤

consumer订阅时,会将订阅信息注册到到服务端;
broker端保存订阅信息的是Map类,key为topic,value主要是tag;
subVersion取当前时间。

留个坑,等研究完Broker端消息的存储再补充。

2)客户端过滤

客户端过滤的代码实现体现在PullAPIWrapper#processPullResult()方法中,这个我们在《Rocket源码分析pullMessage:Consumer是如何从broker拉取消息的?》一文中有详细介绍Consumer拉取消息的流程。
在这里插入图片描述
如果内存中主题订阅信息中tagsSet不为空,并且Consumer订阅时不是ClassFilter会执行Tag过滤;Tag过滤就是单纯的字符串值的比对

为什么服务端做了Tag过滤,客户端还要做呢?

  • 服务端采用Hash的方式存储Tag信息,所以必然会存在hash冲突的情况,导致过滤存在不准确性,所以客户端需要进行精确过滤。
  • 客户度通过tag的字符串值做对比,不相等的tag消息不返回给消费者。

2、SQL过滤原理

存服务端过滤。

留个坑,等研究完Broker端消息的存储再补充。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
RocketMQ可以通过设置顺序消息来保证消息消费顺序。即按照消息的发送顺序来消费消息,这种方式只能保证顺序消息的有序消费,普通消息消费仍然是无序的。 在RocketMQ中,顺序消息是指在同一个消息队列中,按顺序发送的消息。如果我们需要保证消息的顺序消费,需要做以下几个步骤: 1. 创建顺序消息生产者 ```java DefaultMQProducer producer = new DefaultMQProducer("producer_group"); producer.setNamesrvAddr("localhost:9876"); producer.start(); ``` 2. 设置顺序消息 ```java Message message = new Message("topic", "tag", "key", "body".getBytes()); // 设置顺序消息的业务ID,用来保证消息发送的顺序 message.setKeys("order_id"); // 发送顺序消息 SendResult sendResult = producer.send(message, new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> list, Message message, Object o) { // 获取订单ID String orderId = (String) o; // 计算订单ID的hash值 int index = Math.abs(orderId.hashCode()) % list.size(); // 选择消息队列 return list.get(index); } }, "order_id"); ``` 3. 创建顺序消息消费者 ```java DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group"); consumer.setNamesrvAddr("localhost:9876"); consumer.subscribe("topic", "tag"); consumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext context) { // 消费消息 for (MessageExt messageExt : list) { System.out.println(new String(messageExt.getBody())); } // 返回消费状态 return ConsumeOrderlyStatus.SUCCESS; } }); consumer.start(); ``` 在顺序消息的发送端,我们需要设置顺序消息的业务ID,并使用`MessageQueueSelector`选择消息队列。在顺序消息消费端,我们需要注册`MessageListenerOrderly`监听器,按照顺序消费消息,最后返回消费状态。 需要注意的是,为了保证顺序消息的顺序性,我们需要保证同一个业务ID的消息被发送到同一个消息队列中,这样才能保证消息消费顺序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秃秃爱健身

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值