浅“尝”RocKetMQ(一)

RocKetMQ是一个开源的消息中间件,具备低延迟、高吞吐量、高可用性和高可靠性。文章介绍了其核心组件如NameServer、Broker以及生产者、消费者的概念,并通过SpringBoot展示了同步、异步和单向消息发送的代码示例。此外,还探讨了消费者模式,包括拉模式和推模式。
摘要由CSDN通过智能技术生成

一.RocKetMQ介绍

         RocKetMQ是阿里巴巴开源的分布西消息中间件,它具有低延迟、高吞吐量、高可用性和高可靠性等特点,设用于构建具有海量消息堆积和异步解耦功能的应用系统。

上述概念解释:

  • 低延迟:RocKetMQ 提供了高性能的消息传递机制,能够实现低延迟的消息传递。这对于需要实时响应的应用场景非常重要。

  • 高吞吐量:RocKetMQ 能够处理大量的消息并实现高吞吐量的消息传递。这使得它非常适合处理海量消息的应用场景,如大规模的数据处理、日志收集等。

  • 高可用性:RocKetMQ 提供了高可用性的消息传递机制,通过支持主备模式和集群部署,确保消息的可靠传递和系统的高可用性。

  • 高可靠性:RocKetMQ 采用了消息持久化和消息复制机制,确保消息的可靠传递和数据的安全性。即使在出现故障或网络中断的情况下,消息也能够被正确地传递和处理。

  • 异步解耦:RocKetMQ 支持异步消息传递,可以将消息的发送和接收解耦,提高系统的灵活性和可扩展性。这使得应用系统能够更好地应对高并发和大规模的请求。

1.基本概念:

  • 生产者(Producer):  也称消息发布者,是RocKetMQ用来构建并传输消息到服务端的运行实体
  • 主题(Topic): 是RocKetMQ中消息传输的顶级容器,用于标识同一类业务逻辑的消息;是一个逻辑概念,并不是实际的消息容器
  • 消息队列(MessageQueue):是消息存储和传输的实际容器,也是消息的最小存储单位
  • 消息者:(Consumer): 也称消息订阅者,是RocKetMQ中用来接受并处理消息的运行实体
  • 消费者组(ConsumerGroup): 是RocKetMQ中承载多个消费行为一致的消费者负载均衡分组,与消费者不同,消费者组是一个逻辑概念,可以同时订阅和接收多个主题的消息。消费者组是一组具有相同消费逻辑的消费者实例的集合,它们共同消费同一个主题的消息
  • NameServer: 可以理解成注册中心,负责更新和发现Broker服务。在NameServer的集群中,NameServer之间是没有任何通信状态的,它是无状态的
  • Broker: 消息的中转角色,负责消息的存储和转发,接受生产者产生的消息并持久化消息;当用户发送的消息被发送到Broker时,Broker会将消息转发到与之关联的Topic中,以便让更多的接收者进行处理

  •  上图简介: 强烈建议画一遍

生产者会根据不同的业务,将消息发送到不同的Topoc中,存储到不同的Message中,最终由消费者进行消费,多个消费者组成消费者组,多个Broker组成Broker集群,每个Broker中包含多个Topic。

1.1如何理解nameServer

        它就像一个大管家一样,充当了消息队列的路由中心,负责管理和维护消息队列的元数据信息,帮助生产者和消费者找到彼此,并确保消息的可靠传输。

  1. 注册和发现 Broker:当 Broker 启动时,它会向 NameServer 注册自己的信息,包括主题(Topic)和分区(Partition / MessageQueue)的信息。消费者(Consumer)可以通过 NameServer 获取 Broker 的信息,从而订阅和消费消息。

  2. 路由消息:当生产者(Producer)发送消息时,它会将消息发送到指定的 Topic,NameServer 会根据 Topic 的路由规则将消息路由到相应的 Broker 上。

  3. 心跳检测:NameServer 会定期向 Broker 发送心跳检测请求,以确保 Broker 的可用性。

1.2如何理解broker

        可以将 Broker 理解为一个邮局,生产者(Producer)将消息发送到 Broker,消费者(Consumer)从 Broker 接收消息。Broker 负责接收、存储和传递消息,确保消息的可靠性和顺序性。

1.3应用场景:更好的理解概念

短信

  • producer: 手机短信进程,就是生产者
  • message: 包含了编辑的文本短信信息的消息对象
  • consumer: 能够接收到消息的手机短信进程

抢红包

  • producer: 发红包的用户手机客户端进程
  • message: 红包个数 封装的已经包含金额的消息对象
  • consumer: 抢红包用户手机客户端进程中,通过点击开红包,触发的代码片

注:RocKetMQ的安装和配置可自行查找相关文献

二.启动--以windows系统为例

  • 进入mq解压后的bin目录下,进入cmd,运行以下命令启动nameServer和broker
  • 注:再启动前保证自己的JDK环境变量已经配置好
  • 执行:mqnamesrv.cmd  启动nameServer
  • 执行:mqbroker.cmd -n localhost:9876   启动broker,连接指定的服务器
  • rocketmq团队提供了一个查询rocket状态数据的仪表盘系统:rocket-dashboard,可自行进行下载,在cmd中执行对应的jar包即可
  • 以下代码基于mq5.0版本

三.基于SpringBoot演示MQ基础代码示例

导入依赖:

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>

1.生产者代码示例 

1.1同步发送

是最常用的方式,是指消息发送方发出一条消息后,会在收到服务端同步响应之后才发送下一条的通讯方式,可靠的同步传输被广泛应用于各种场景,如重要的通知消息、短消息通知等。

自我理解:等待消息返回后在进行下面的操作,应用于重要场景,“两端都应做到句句有回应,才能感情稳定”

官网图片:

  • 代码示例:创建测试类,进行编写
@Test
    public void sendTest1() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        //本次代码实现同步发送
        //准备一个生产者对象,并对其分组命名
        DefaultMQProducer producer = new DefaultMQProducer("sndTest1");
        //连接nameserver,同启动broker时的 mqbroker.cmd -n localhost:9876的地址保持一致
        producer.setNamesrvAddr("localhost:9876");
        //生产者producer于nameServer建立连接
        producer.start();



        //将消息同步发送
        for (int i=0;i<3;i++){
            //创建一个消息对象,主题topic按业务起名即可,
            //tag对消息进行在分类,RocketMQ可以在消费端对tag进行过滤。
            //携带消息的body
            Message message =
                    new Message("topic",i+"sndTest","同步发送消息".getBytes(StandardCharsets.UTF_8));


            //通过 SendResult 对象获取发送结果和相关信息
            SendResult send = producer.send(message);
            //获取发送状态
            System.out.println("发送状态:"+send.getSendStatus());
            //通过 getMsgId 方法获取消息的唯一标识
            send.getMsgId();
            //通过 getMessageQueue 方法获取消息所在的主题、队列和 Broker 的信息
            System.out.println("消息到达主题,队列,broker信息:"+send.getMessageQueue());
        }
        //关闭 Producer 的主要原因有以下几点:
        //资源释放:关闭 Producer 可以释放占用的资源,包括网络连接、线程等。这样可以避免资源的浪费和占用。
        //优雅退出:关闭 Producer 可以保证程序的正常退出。在关闭 Producer 之前,可以先发送一个特殊的消息,
            // 通知 Consumer 停止消费,然后再关闭 Producer。这样可以避免消息丢失和消费者无法正常停止的问题。
        //避免内存泄漏:如果不关闭 Producer,可能会导致内存泄漏问题。
            // Producer 内部可能会缓存一些数据,如果不及时释放,可能会导致内存占用过高。
        producer.shutdown();

    }
  • 启动MQ,执行代码后查看MQ图形化界面,查看主题和消息,则会出现以下内容代表执行成功

 

1.2异步发送

  • 异步发送是指发送方发出一条消息后,不等服务端返回响应,接着发送下一条消息的通讯方式。

  • 消息发送方在发送了一条消息后,不需要等待服务端响应即可发送第二条消息,发送方通过回调接口接收服务端响应,并处理响应结果。异步发送一般用于链路耗时较长,对响应时间较为敏感的业务场景。例如,视频上传后通知启动转码服务,转码完成后通知推送转码结果等。

代码示例:

 @Test
    public void sendTest2() throws MQClientException, RemotingException, InterruptedException {
        //本次代码实现异步发送
        //准备一个生产者对象,并对其分组命名
        DefaultMQProducer producer = new DefaultMQProducer("sndTest2");
        //连接nameserver,同启动broker时的 mqbroker.cmd -n localhost:9876的地址保持一致
        producer.setNamesrvAddr("localhost:9876");
        //生产者producer于nameServer建立连接
        producer.start();

        //消息发送失败的重试次数
        producer.setRetryTimesWhenSendAsyncFailed(0);




        //计数器
        final CountDownLatch countDownLatch = new CountDownLatch(100);
        for (int i = 0; i < 100; i++) {
            final int index = i;
            Message message = new Message("Simple", "TagA", (i + "sndTest2").getBytes(StandardCharsets.UTF_8));
            // 异步发送消息, 发送结果通过callback返回给客户端
            producer.send(message, new SendCallback() {
                //发送成功回调
                @Override
                public void onSuccess(SendResult sendResult) {

                    System.out.println(index + "_消息发送成功"+sendResult);
                    countDownLatch.countDown();
                }

                //发送失败回调
                @Override
                public void onException(Throwable throwable) {

                    System.out.println(index + "_消息发送失败");
                    throwable.printStackTrace();
                    countDownLatch.countDown();
                }
            });

        }

        //异步发送,如果要求可靠传输,必须要等回调接口返回明确结果后才能结束逻辑,否则立即关闭Producer可能导致部分消息尚未传输成功
        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.shutdown();


    }

1.3单向模式发送

  • 发送方只负责发送消息,不等待服务端返回响应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。
  • 与同步发送在代码上的唯一区别就是发送没有返回结果

 代码示例:

@Test
    public void sendTest3() throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
        //本次代码实现单向发送
        //准备一个生产者对象,并对其分组命名
        DefaultMQProducer producer = new DefaultMQProducer("sndTest3");
        //连接nameserver,同启动broker时的 mqbroker.cmd -n localhost:9876的地址保持一致
        producer.setNamesrvAddr("localhost:9876");
        //生产者producer于nameServer建立连接
        producer.start();

        //将消息同步发送
        for (int i=0;i<2;i++){
            //创建一个消息对象,主题topic按业务起名即可,
            //tag对消息进行在分类,RocketMQ可以在消费端对tag进行过滤。
            //携带消息的body
            Message message =
                    new Message("Simple3",i+"sndTest3","同步发送消息".getBytes(StandardCharsets.UTF_8));


            //通过 SendResult 对象获取发送结果和相关信息
            producer.sendOneway(message);
            System.out.println(i+"_消息发送成功");


        }
        //关闭 Producer 的主要原因有以下几点:
        //资源释放:关闭 Producer 可以释放占用的资源,包括网络连接、线程等。这样可以避免资源的浪费和占用。
        //优雅退出:关闭 Producer 可以保证程序的正常退出。在关闭 Producer 之前,可以先发送一个特殊的消息,
        // 通知 Consumer 停止消费,然后再关闭 Producer。这样可以避免消息丢失和消费者无法正常停止的问题。
        //避免内存泄漏:如果不关闭 Producer,可能会导致内存泄漏问题。
        // Producer 内部可能会缓存一些数据,如果不及时释放,可能会导致内存占用过高。
        producer.shutdown();
    }

2.消费者代码示例

  • 拉模式--Pull: 消费者主动去Broker上拉取消息
  • 推模式--Push: 消费者等待Broker把消息推送过来

怎么理解以上两种模式?

  1. 数据流向:推模式中,数据的流向是从数据的提供者向数据的接收者;而拉模式中,数据的流向是从数据的接收者向数据的提供者。

  2. 主动性和被动性:推模式中,数据的提供者主动发送数据,而数据的接收者被动接收数据;而拉模式中,数据的接收者主动请求数据,而数据的提供者被动提供数据。

  3. 控制权:推模式中,数据的提供者决定何时发送数据,而数据的接收者无法控制数据的发送频率和数量;而拉模式中,数据的接收者决定何时请求数据,而数据的提供者被动提供数据。

  4. 实时性:推模式适用于实时性要求较高的场景,因为数据的提供者主动发送数据,可以及时传递最新的数据;而拉模式适用于实时性要求不高的场景,因为数据的接收者可以根据自身需求灵活控制数据的获取。

  5. 灵活性:推模式在数据的接收方面较为被动,无法灵活控制数据的获取;而拉模式在数据的获取方面较为灵活,可以根据需求灵活控制数据的获取。

2.1推Push代码示例:

@Test
    public void consumerTest1() throws MQClientException {
        // 创建一个默认的消息消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("SimpleConsumer");
        // 设置NameServer地址
        consumer.setNamesrvAddr("localhost:9876");
        // 订阅主题和标签,消费所有消息
        consumer.subscribe("Simple", "*");
        // 设置消息监听器
        consumer.setMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {

                 使用for循环遍历消息列表,处理每条消息
                for (int i = 0; i < list.size(); i++) {
                    System.out.println(i + "_消费成功" + new String(list.get(i).getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //启动消费者
        consumer.start();
        System.out.println("启动成功");
        while (true) ;

    }

3.拉Pull模式示例:

在RocketMQ中有两种Pull方式,一种是比较原始Pull Consumer,它不提供相关的订阅方法,需要调用pull方法时指定队列进行拉取,并需要自己更新位点。另一种是Lite Pull Consumer,它提供了Subscribe和Assign两种方式,使用起来更加方便。

  • 拉取完成后会返回拉取结果PullResult,PullResult中的PullStatus表示结果状态:

FOUND表示拉取到消息,NO_NEW_MSG表示没有发现新消息,NO_MATCHED_MSG表示没有匹配的消息,OFFSET_ILLEGAL表示传入的拉取位点是非法的,有可能偏大或偏小。如果拉取状态是FOUND,我们可以通过pullResultgetMsgFoundList方法获取拉取到的消息列表。最后,如果消费完成,通过updateConsumeOffset方法更新消费位点。

3.1PullConsumer示例:

  • 首先要理解:
  • 设置Name Server的地址时,是用于获取Broker的信息
  • Broker中,有多个Topic主题
  • 每个主题中包含多个(MessageQueue)消息队列
  • 注:此处不理解可以看基础概念处的图片
  • 偏移量概念:

偏移量(Offset)是用来标识消息在消息队列中的位置的一个值。每个消息都有一个唯一的偏移量,用于表示消息在队列中的顺序。

可以将偏移量理解为消息队列中的一个索引,类似于数组中的下标。偏移量越大,表示消息在队列中的位置越靠后;偏移量越小,表示消息在队列中的位置越靠前。

偏移量的作用是用来记录消费者消费消息的进度。当消费者消费一条消息后,会将消费的偏移量记录下来。这样,在下一次消费时,消费者就可以从上次消费的偏移量开始继续消费,确保消息的顺序性和不重复消费。

通过偏移量,消费者可以知道自己消费到了哪个位置,从而实现消息的有序消费和断点续传的功能。

代码示例:

@Test
    public void consumerPull() throws MQClientException {
        //创建一个Pull Consumer对象,并指定一个唯一的消费者组名
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        //定义一个Set的主题集合
        Set<String> topics = new HashSet<>();
        topics.add("Simple3");
        //消费者要拉取的集合们
        consumer.setRegisterTopics(topics);
        consumer.start();
        while (true) {
            //获取每一个主题
            consumer.getRegisterTopics().forEach(n -> {
                try {
                    //拿到一个topic对应的messageQueue(消息队列)
                    Set<MessageQueue> messageQueues = consumer.fetchSubscribeMessageQueues(n);
                    messageQueues.forEach(l -> {
                        try {
                            //获取消息队列的偏移量--从内存获取
                            long offset = consumer.getOffsetStore().readOffset(l, ReadOffsetType.READ_FROM_MEMORY);
                            //没有获取到为-1,改为从队列获取
                            if (offset < 0) {
                                offset = consumer.getOffsetStore().readOffset(l, ReadOffsetType.READ_FROM_STORE);
                            }
                            //改为队列的最大偏移量
                            if (offset < 0) {
                                offset = consumer.maxOffset(l);
                            }
                            //兜底的
                            if (offset < 0) {
                                offset = 0;
                            }
                            // 拉取消息,参数分别为:队列、消息过滤表达式、起始偏移量、拉取的消息数量
                            PullResult pullResult = consumer.pull(l, "*", offset, 32);
                            System.out.println("循环拉取成功_" + pullResult);
                            //判断拉取消息的状态
                            switch (pullResult.getPullStatus()) {
                                //拉取成功,其他状况可以用不同的case去处理
                                case FOUND:
                                    pullResult.getMsgFoundList().forEach(k -> {
                                        System.out.println("消费成功_" + k);
                                    });
                            }
                        } catch (MQClientException e) {
                            throw new RuntimeException(e);
                        } catch (RemotingException e) {
                            throw new RuntimeException(e);
                        } catch (MQBrokerException e) {
                            throw new RuntimeException(e);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    });
                } catch (MQClientException e) {
                    e.printStackTrace();
                }
            });
        }
    }

3.2LitePullConsumer示例:

  • Subscribe方式:随机获取一个消息队列
@Test
    public void consumerListPull() throws MQClientException {
        DefaultLitePullConsumer litePullConsumer =
                new DefaultLitePullConsumer("LifePullConsumer");
        litePullConsumer.setNamesrvAddr("localhost:9876");
        //随机获取
        litePullConsumer.subscribe("Simple3", "*");
        litePullConsumer.start();
        while (true){
            List<MessageExt> poll = litePullConsumer.poll();
            System.out.println("消息拉取成功");
            poll.forEach(n->{
                System.out.println("消息消费成功_" + n);
            });

        }
    }
  • Assign方式:指定获取一个消息队列
 @Test
    public void consumerListPull1() throws MQClientException {
        DefaultLitePullConsumer litePullConsumer =
                new DefaultLitePullConsumer("LifePullConsumer1");
        litePullConsumer.setNamesrvAddr("localhost:9876");
        litePullConsumer.start();
        //获取主题中的消息队列集合
        Collection<MessageQueue> messages = litePullConsumer.fetchMessageQueues("Simple3");
        ArrayList<MessageQueue> messageQueues = new ArrayList<>(messages);
        //将消息队列集合分配给消费者,表示消费者将从这些消息队列中消费消息
        litePullConsumer.assign(messageQueues);
        //设置消费者从指定消息队列的指定偏移量(20)开始消费消息。这里的偏移量表示消息在消息队列中的位置,从 0 开始计数
        litePullConsumer.seek(messageQueues.get(0), 10);


        while (true){
            List<MessageExt> poll = litePullConsumer.poll();
            System.out.println("消息拉取成功");
            poll.forEach(n->{
                System.out.println("消息消费成功_" + n);
            });

        }
    }

终于写完了!!!!  后续会更新顺序消息 延迟消息 批量消息 事务消息等!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值