【RocketMQ 生产者消费者】- 同步、异步、单向发送消费消息


本文章基于 RocketMQ 4.9.3

1. 前言

下面继续更新 RocketMQ 系列的文章,前面这些文章我们已经讲述了 RocketMQ 消息的存储,那接下来就需要从消息发送和消费入手来逐步介绍 RocketMQ 的各种消息,如延时消息、事务消息 … 本篇文章我们就来学习下 RocketMQ 的消息发送和消费,也算是为后面的源码分析做下铺垫。


2. 同步发送消息

在这里插入图片描述

下面我们来看下 RocketMQ 如何同步发送消息,这里我就不用 SpringBoot 了,直接创建 DefaultMQProducer 来发送。

public class SyncProducer {
    public static void main(String[] args) throws Exception {
        // 创建生产者, 设定生产者组
        DefaultMQProducer producer = new DefaultMQProducer("producerGroup");
        // 设置 NameServer 地址
        producer.setNamesrvAddr("localhost:9876");
        // 启动生产者
        producer.start();
        for (int i = 0; i < 10; i++) {
            // 构建消息, 设置 topic 为 TopicTest, tags 为 SyncProducerA
            Message msg = new Message("TopicTest", "SyncProducerA", ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            // 发送消息到一个 Broker, 阻塞等待发送结果
            SendResult sendResult = producer.send(msg);
            // 通过 sendResult 返回消息是否成功送达
            System.out.printf("%s%n", sendResult);
        }
        // 发送完之后关闭生产者
        producer.shutdown();
    }
}

代码流程比较简单:

  1. 构建生产者,设置生产者所属的生产者组。
  2. 设置 NameServer 的地址,生产者要发送消息到 broker,那么 broker 地址怎么来呢?就要从 NameServer 拉取了,如果由多个 NameServer,就用 ; 隔开,比如:127.0.0.1:9876;127.0.0.2:9876。
  3. 启动生产者。
  4. 创建消息,设置消息的 topic,设置消息的 Tag,以及消息体(消息的具体内容)。
  5. 最后使用 send 接口发送,因为这里是同步发送,所以会阻塞等到返回结果,返回结果就是 SendResult,SendResult 包括 SEND_OK(发送成功), FLUSH_DISK_TIMEOUT(刷盘超时), FLUSH_SLAVE_TIMEOUT(同步从节点超时), SLAVE_NOT_AVAILABLE(从节点不可用),如果发送失败会 抛出异常,所以对于同步发送,需要手动捕获发送异常,做业务兜底

3. 异步发送消息

上面是同步发送消息的逻辑,那么下面我们来看下异步发送的逻辑,所谓异步发送就是不需要阻塞等待返回结果,而是可以通过设置回调方法来处理异步消息的回调,所以异步发送的核心就是:异步发送回调接口(SendCallback)。

异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应,下面来看下异步发送的流程。
在这里插入图片描述

public class AsyncProducer {
    public static void main(String[] args) throws Exception {
        // 创建生产者, 设定生产者组
        DefaultMQProducer producer = new DefaultMQProducer("producerGroup");
        // 设置 NameServer 地址
        producer.setNamesrvAddr("localhost:9876");
        // 启动生产者
        producer.start();
        // 异步发送失败重试次数, 异步重试不会选择其他 broker, 仅在同一个 broker 上做重试, 不保证消息不丢
        producer.setRetryTimesWhenSendAsyncFailed(0);
        int messageCount = 10;
        final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
        for (int i = 0; i < messageCount; i++) {
            try {
                final int index = i;
                // 构建消息, 设置 topic 为 TopicTest, tags 为 ASyncProducerA
                Message msg = new Message("TopicTest", "ASyncProducerA", "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                // 异步发送消息, 发送结果通过 SendCallback 进行回调
                producer.send(msg, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        // 消息发送成功回调
                        System.out.printf("%-10d OK %s %n", index,
                                sendResult.getMsgId());
                        countDownLatch.countDown();
                    }

                    @Override
                    public void onException(Throwable e) {
                        // 消息发送出异常
                        System.out.printf("%-10d Exception %s %n", index, e);
                        e.printStackTrace();
                        countDownLatch.countDown();
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
                countDownLatch.countDown();
            }
        }
        // 异步发送, 如果要求可靠传输, 这里等待 5s 之后再关闭, 否则直接关闭生产者
        countDownLatch.await(5, TimeUnit.SECONDS);
        // 关闭生产者
        producer.shutdown();
    }
}

代码流程比较简单:

  1. 构建生产者,设置生产者所属的生产者组。
  2. 设置 NameServer 的地址,生产者要发送消息到 broker,那么 broker 地址怎么来呢?就要从 NameServer 拉取了,如果由多个 NameServer,就用 ; 隔开,比如:127.0.0.1:9876;127.0.0.2:9876。
  3. 启动生产者。
  4. 创建消息,设置消息的 topic,设置消息的 Tag,以及消息体(消息的具体内容)。
  5. 最后使用 send 接口发送,异步发送和同步发送上面几步流程一模一样,只是在发送的时候需要设置 SendCallback,所以异步发送必须要设置这个 SendCallback,不设置没办法回调,onSuccess 是发送成功的回调,onException 是发送异常的回调,可以在这里面设置自己的业务逻辑。

4. 单向发送消息

在一些对可靠性要求不高的场景如日志收集,我们并不需要接收消息的回调,而同步和异步都需要设置接收返回值,所以就有了第三种发送方式:单向发送。单向发送用于某些耗时非常短,但对可靠性要求并不高的场景,同时这类的消息发送耗时比较小,一般在微秒级别。所谓的单向发送就是生产者只管发送消息,不需要接收返回值,也不用设置回调函数,不等待响应。
在这里插入图片描述

public class OnewayProducer {
    public static void main(String[] args) throws Exception{
        // 创建生产者, 设定生产者组
        DefaultMQProducer producer = new DefaultMQProducer("producerGroup");
        // 设置 NameServer 地址
        producer.setNamesrvAddr("localhost:9876");
        // 启动生产者
        producer.start();
        for (int i = 0; i < 10; i++) {
            // 构建消息, 设置 topic 为 TopicTest, tags 为 OnewayProducerA
            Message msg = new Message("TopicTest", "OnewayProducerA", ("Hello RocketMQ OnewayProducer" + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            // 发送单向消息,没有任何返回结果
            producer.sendOneway(msg);

        }
        // 后续不发送了就关闭生产者
        producer.shutdown();
    }
}

当然了,这个方法也是会抛出异常的,所以如果发送过程中出现异常还是需要用户手动去捕获的。


3. 消费消息

下面来看下消费者是如何消费消息的,消费者分为并发消费和顺序消费,下面演示下并发消费。

public class Consumer {

    public static void main(String[] args) throws InterruptedException, MQClientException {

        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("testGroupConsumer");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe("TopicTest", "OnewayProducerA");
        consumer.registerMessageListener(new MessageListenerConcurrently() {

            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), new String(msg.getBody(), StandardCharsets.UTF_8));
                }

                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();

        System.out.printf("Consumer Started.%n");
    }
}

上面我们使用消费者消费了单向生产者发送的消息,来看下输出结果:
在这里插入图片描述


4. 生产者重要参数

上面相关的例子就写完了,下面来看下 Producer 生产者的几个参数:

  • topic: 表示要发送的消息的主题,比如一个项目中不同类型的消息如顺序消息、事务消息等可以用不同的 topic,又比如没有关联的业务比如交易业务、物流业务等都可以用不同的 topic 进行区分。
  • tags: 表示要发送的消息的标签,topic 和 tags 的主题是一个 topic 下面有多个 tag,例如上面的交易业务里面水果、蔬菜 … 不同类型的物品交易就可以用不同 tag,总而延时 tags 就是对 topic 下面的进一步划分。
  • keys: 表示每条消息在业务层面的唯一标识码,方便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应用可以通过 topic、key 来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证 key 尽可能唯一,这样可以避免潜在的哈希冲突。
  • body: 表示要发送的消息的消息体。

上面就是生产者的几个参数,通过 topic 和 tags 就能确定这条消息的属性,相应的消费者也可以订阅 topic 下面的某个 tag 或者多个 tag 来精确消费消息,而通过 keys 可以确定消息的唯一性,用于消息查询。

对与生产者同步发送消息,返回值 SendResult 有几个类型:

  • SEND_OK: 消息发送成功。要注意的是消息发送成功也不意味着它是可靠的。要确保不会丢失任何消息,还应启用同步 Master 服务器或同步刷盘,即 SYNC_MASTERSYNC_FLUSH
  • FLUSH_DISK_TIMEOUT: 消息发送成功但是服务器刷盘超时。此时消息已经进入服务器队列(内存),只有服务器宕机,消息才会丢失。消息存储配置参数中可以设置刷盘方式和同步刷盘时间长度,如果 Broker 服务器设置了刷盘方式为同步刷盘,即 FlushDiskType=SYNC_FLUSH(默认为异步刷盘方式),当 Broker 服务器未在同步刷盘时间内(默认为 5s)完成刷盘,则将返回该状态——刷盘超时。
  • FLUSH_SLAVE_TIMEOUT: 消息发送成功,但是服务器同步到 Slave 时超时。此时消息已经进入服务器队列,只有服务器宕机,消息才会丢失。如果 Broker 服务器的角色是同步 Master,即 SYNC_MASTER(默认是异步 Master 即ASYNC_MASTER),并且从 Broker 服务器未在同步刷盘时间(默认为 5 秒)内完成与主服务器的同步,则将返回该状态——数据同步到Slave 服务器超时。
  • SLAVE_NOT_AVAILABLE: 消息发送成功,但是此时 Slave 不可用。如果 Broker 服务器的角色是同步Master,即 SYNC_MASTER(默认是异步 Master 服务器即 ASYNC_MASTER),但没有配置从节点服务器或者主节点当前 CommitLog 最新消息偏移量距离上一次同步到从服务器的偏移量超过了 256MB,则将返回该状态——无 Slave 服务器可用(Slave 服务器不可用)。

5. 消费者相关概念

5.1 消费者组

消费者可以通过 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("testGroupConsumer"); 来设置这个消费者所在的组,不同的消费者组一般都会消费不同的 topic,但是一个消费者内的消费者需要保持订阅信息一致,这个在后面简述订阅关系源码的时候也会讲到。


5.2 并发消费和顺序消费

RocketMQ 的消费者分为并发消费和顺序消费,当然上面演示用的是并发消费,所以可以看到消费者的监听器类型是 MessageListenerConcurrently,如果对消息消费顺序没有要求的时候可以用并发消费,顺序消费会在后面介绍源码的时候再介绍相关例子,这里只是抛出这么一个概念。


5.3 消费模式

下面再来介绍下消费者的消费模式:集群模式广播模式

// 广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);

// 集群模式
consumer.setMessageModel(MessageModel.CLUSTERING);
  • 广播模式就是每一条消息都会被一个消费者组内的所有消费者消费,相当于一条消息至少被消费 n 次,这个 n 就是消费者组的消费者数量,为什么是至少呢?因为还有消息失败重试。
    在这里插入图片描述

  • 集群模式就是消费者组内的消费者会分担消费不同消息队列下面的消息,相当于说一条消息只需要被消费者组内的一个消费者消费即可,我们一般用的就是集群模式,默认也是集群模式,如果需要提高消息速率,集群模式下直接通过增加消费者组里面的消费者即可。
    在这里插入图片描述


5.4 消费者的 PUSH 模式和 PULL 模式

Push 和 Pull 也算是消费者比较经典的两种模式了,代表了消费者处理消息的不同行为:

  • Pull 就是拉取消息,意思就是由消费者决定什么时候拉取消息,拉取消息的频率,拉取消息的点位,这些都是用户来决定的,优点就是用户自己可以控制拉取消息的速率,不至于拉取频率太高导致消费堆积,而拉取频率低又消费不及时。
  • Push 模式就是服务端主动推送消息给客户端,其实说是服务端推送,实际上 Push 就是 Pull 的一层封装, Pull 模式是用户自己去拉取消息,Push 模式就是客户端帮用户控制拉取频率,消费者需要做的就是消费,当然了这样坏处就是客户端如果没有做好流控,就有可能导致消息堆积导致系统崩溃。

5.5 消息消费过滤

5.5.1 Tag 过滤

RocketMQ 的消息生产者在发送消息时可以为每条消息设置一个或多个 Tag,这些 Tag 会随着消息一起存储在 Broker 中。消费者在订阅消息时,可以通过指定 Tag 来过滤消息,Broker 在推送消息的时候也会根据 Tag 来先过滤,但是 Broker 的过滤是对 Tag 的 hashCode 过滤,实际上推送到消费者之后消费者也会再次过滤,不过消费者的过滤是更精确的 equals 过滤,这都是后面的源码了,下面是从官网拿过来的一个图。
在这里插入图片描述
从上面图中其实大家也能看到了,一个 topic 下面会有多个 tag,而不同的系统消费者可以通过订阅不同的 tag 来消费不同的消息,比如物流系统关心的就是物流消息,支付系统关心的是支付消息,而交易系统关心的是订单和支付消息,最后实时计算系统关心所有消息,既然消费者可以关注一个或者多个 tag,那么 tag 过滤肯定有自己的语法。

  • 单个 Tag: consumer.subscribe(“TopicTest”, “TagA”) 用于指定单个 Tag,表示只消费 TopicTest 主题下 TagA 的消息。
  • 多个 Tag: consumer.subscribe(“TopicTest”, “TagA || TagB”) 用于指定多个 Tag,中间使用 || 分割,表示消费 TopicTest 主题下 TagA 或 TagB 的消息。
  • 所有 Tag: consumer.subscribe(“TopicTest”, “*”) 用于订阅主题下的所有 Tag 的消息,* 就代表所有 tag。

5.5.2 SQL92 过滤

5.5.2.1 例子

在 RocketMQ 中,除了使用 Tag 进行消息过滤,还可以使用 SQL 92 语法进行更灵活的消息过滤。SQL 92 过滤允许消费者根据消息的属性进行筛选,这样可以实现更复杂的过滤逻辑。生产者在发送的时候可以设置消息的自定义属性,消费者消费的时候就可以通过 SQL 92 过滤表达式对这个属性进行过滤,下面来看个例子。

要使用 SQL92,需要在 broker.config 配置文件中设置启动 SQL92 的配置项。

enablePropertyFilter=true

首先是生产者,生产者生产 10 条消息,然后设置属性 index 为下标 i。

public class SQL92Producer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("producerGroup");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        for (int i = 0; i < 10; i++) {
            Message msg = new Message("SQL92Test", "test", ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            msg.putUserProperty("index", i + "");
            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        }
        // 发送完之后关闭生产者
        producer.shutdown();
    }
}

接着我们启动消费者,消费者主要消费 index > 4 的消息。

public class SQL92Consumer {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
        consumer.setNamesrvAddr("localhost:9876");

        // 使用SQL 92语法进行消息过滤,只消费 index > 4 的消息
        consumer.subscribe("SQL92Test", MessageSelector.bySql("index > 4"));

        // 注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), new String(msg.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 启动消费者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

接着看生产者和消费者的输出。
在这里插入图片描述
在这里插入图片描述


5.5.2.2 SQL 92过滤支持的语法和操作符
  • 比较操作符:>, >=, <, <=, =, <>(不等于)。例如:price > 100。
  • 逻辑操作符: AND, OR, NOT。例如:price > 100 AND category = ‘electronics’。
  • 字符串比较: 可以使用=和<>进行字符串比较。例如:category = ‘books’。
  • IS NULL 或者 IS NOT NULL

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


6. 小结

RocketMQ 官方文档,左边是官方给的文档,大家有需要可以到这里面去看,里面比较详细介绍了生产者和消费者这一块。本文作为一个引子将生产者和消费者简单介绍了一下,还有一些消费者的概念比如消费点位,Push 消费者和 Pull 消费者的具体区别,到了后面源码都会一一介绍。





如有错误,欢迎指出!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值