RocketMQ

 官方文档

Docker 部署 RocketMQ | RocketMQ (apache.org)

一  为什么要学MQ

我在这里看到的解释蛮好的:

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                   
原文链接:https://blog.csdn.net/xianren95/article/details/121924194

应用场景

消息队列在互联网技术开发中使用非常广泛,主要用于解决应用耦合,消息分发,流量削锋等问题。以下介绍消息队列在实际应用中常用的使用场景(都以用户下单为例)。

应用解耦

场景说明:

一个简单的用户下单后根据支付金额增加用户积分的场景,传统模式下需要订单模块调用积分模块接口,这样的话订单模块与积分模块就形成了系统耦合,一旦积分模块有修改或出现异常就会影响订单模块功能。引入消息队列方案后, 用户下单成功后,将消息写入消息队列就可以了。积分模块只需要订阅下单消息,从消息队列中获取数据进行消费,这样订单模块和积分模块都只要专注实现自己的功能实现,实现解耦。

数据分发(异步)

场景说明:

用户下单后日志模块要记录下单日志,库存模块需要减少相应库存,积分模块需要增加用户积分等由下单成功引起的其余模块的业务操作,这个时候可以通过消息队列可以让数据在多个系统更加之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可。

削峰填谷

场景说明:

双十一期间系统受到的请求流量猛增,有可能会将系统压垮。传统做法是为了保证系统的稳定性,一般是增加服务器配置、新增服务器做负载均衡这样的话在正常时间段都能满足服务的情况下采用这种做法无疑是对服务器性能的一种浪费,并不划算!另一种做法是如果系统负载超过阈值,就会阻止用户请求,但在流量高峰时这会影响用户体验。通过消息队列就可以完美解决这个问题,引入消息队列方案后可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提到系统的稳定性和用户体验。

1,要做到系统解耦,当新的模块进来时,可以做到代码改动最小;  能够解耦

2,设置流程缓冲池,可以让后端系统按自身吞吐能力进行消费,不被冲垮; 能够削峰,限流

3,强弱依赖梳理能把非关键调用链路的操作异步化并提升整体系统的吞吐能力;能够异步

Mq的作用  削峰限流 异步 解耦合

二  主流MQ介绍

三  RocketMQ介绍

1  定义

中间件(缓存中间件  redis 数据库中间件 mycat  canal   消息中间件mq )

面向消息的中间件(message-oriented middleware) MOM能够很好的解决削峰限流 异步 解耦合的问题。

是指利用高效可靠的消息传递机制进行与平台无关(跨平台)的数据交流,并基于数据通信来进行分布式系统的集成。

通过提供消息传递和消息排队模型在分布式环境下提供应用解耦,弹性伸缩,冗余存储,流量削峰,异步通信,数据同步等

大致流程

发送者把消息发给消息服务器[MQ],消息服务器把消息存放在若干队列/主题中,在合适的时候,消息服务器会把消息转发给接受者。在这个过程中,发送和接受是异步的,也就是发送无需等待,发送者和接受者的生命周期也没有必然关系在发布pub/订阅sub模式下,也可以完成一对多的通信,可以让一个消息有多个接受者[微信订阅号就是这样的]

2  RocketMQ重要概念

Producer:消息的发送者,生产者;举例:发件人

Consumer:消息接收者,消费者;举例:收件人

Broker:暂存和传输消息的通道;举例:快递

NameServer:管理Broker;举例:各个快递公司的管理机构 相当于broker的注册中心,保留了broker的信息。类似于nacos

Queue:队列,消息存放的位置,一个Broker中可以有多个队列,是一个真实存在的,里面放着消息的地址,在topic内

Topic:主题,消息的分类,是个虚拟的结构。主题内含有多个队列,轮巡发送。在Broker内

ProducerGroup:生产者组

ConsumerGroup:消费者组,多个消费者组可以同时消费一个主题的消息

消息发送的流程是,Producer询问NameServer,NameServer分配一个broker。broker会将Producer发来的消息储存在队列里,并按一定主题将不同队列分类。然后Consumer也要询问NameServer,得到一个具体的broker,然后消费消息

四  docker安装RocketMQ   

官方文档

Docker 部署 RocketMQ | RocketMQ (apache.org)

csdn中亲测有效的一篇

基于Docker安装RockerMQ【保姆级教程、内含图解】_docker安装rocket mq-CSDN博客

五  快速入门

1  分别测试生产者和消费者

我们先搞清楚消息发送和监听的流程,然后我们在开始敲代码

消息生产者

1.创建消息生产者producer,并制定生产者组名

2.指定Nameserver地址

3.启动producer

4.创建消息对象,指定主题Topic、Tag和消息体等

5.发送消息

6.关闭生产者producer

消息消费者

1.创建消费者consumer,制定消费者组名

2.指定Nameserver地址

3.创建监听订阅主题Topic和Tag等

4.处理消息

5.启动消费者consumer

package com.mq.learnrocketmq.demo;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.junit.Test;

import java.nio.charset.StandardCharsets;
import java.util.List;

public class PAndC {

    /**
     * 测试生产者
     *
     * @throws Exception
     */
    @Test
    public void testProducer() throws Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("test-group");
        // 设置nameServer地址
        producer.setNamesrvAddr("192.168.6.131:9876");
        // 启动实例
        producer.start();
        for (int i = 0; i < 10; i++) {
            // 创建消息
            // 第一个参数:主题的名字
            // 第二个参数:消息内容
            Message msg = new Message("TopicTest", ("Hello RocketMQ " + i).getBytes());
            SendResult send = producer.send(msg);
            System.out.println(send);
        }
        // 关闭实例
        producer.shutdown();
    }

    /**
     * 测试消费者
     *
     * @throws Exception
     */
    @Test
    public void testConsumer() throws Exception {
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.6.131:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("TopicTest", "*");
        // 注册一个消费监听 MessageListenerConcurrently 是多线程消费,默认20个线程
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                System.out.println(Thread.currentThread().getName() + "----" + msgs);
                // 返回消费的状态 如果是CONSUME_SUCCESS 则成功,若为RECONSUME_LATER则该条消息会被重回队列,重新被投递
                // 重试的时间为messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
                // 也就是第一次1s 第二次5s 第三次10s  ....  如果重试了18次 那么这个消息就会被终止发送给消费者
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
//                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        });
        // 这个start一定要写在registerMessageListener下面
        consumer.start();
        // 阻塞主线程,保证监听线程持续工作
        System.in.read();
    }

}

2  详细解读

2.1  消费者组

我们在创建生产者对象时,为他赋予了一个生产者组:“test-group”

 // 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-group");

对于一个生产者组,没有特定的要求。一个生产者组里面可以含有多个生产者,每个生产者发送消息的主题也可以不一样。

2.2  生产者组

在创建消费者对象的时候,我们设置了消费者的组别

// 创建默认消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");

消费者组有一定的限制:

消费者组内的消费者订阅关系必须保持一致,以免出现消息的混乱。就是说,比如我消费者c1和c2在一个组,那么他们消费的主题必须是同一个。及订阅的对象主题是同一个

// 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("TopicTest", "*");

2.3  消息消费特点

解读:

同一个组内如果有多个对象,我们的消息将会采用负载均衡来为每一个消费者发送信息。

若有多个组来消费同一个主题,当发送一个消息时,我们主题会以组为单位为每一个组分发同一个消息。就是广播模式

2.4  生产者组内负载均衡

我们一个主题含有四个队列。

我们生产者已轮训的方式向4个消息队列发送消息。

假如我们一个消费者组里面含有两个消费者,那么两个消费者分别负责两条队列。如下图,c1就只负责0和1编号的队列,无论是否有消息。c2就只负责2和3编号的队列。这就是消费者组的负载均衡。

这是同一个组下有三个消费者的负载均衡,就是说会有一个组接收两个队列的消息,不一定是c1

当一个消费者组内消费者数量有超过队列数量的时候,多出来的那一个消费者是接收不到消息的,处于开摆状态。所以我们会要求队列的数量大于等于消费者组内消费者的数量。

2.5  位点

代理者就是,我们生产者发消息给MQ,broker接收,broker就是代理者。消费者就是我们的程序,消费消息的程序。

代理者位点:当我们生产者发送消息给mq时,代理者位点会根据消息而移动。就是说,我放入消息3,位点就指向消息三。

消费者位点:当消费者消费了一条消息成功后,消费者位点会往后移一个。比如说,我们消费了0号消息成功后,消费者位点就会往后移一位,移到1处。

差值就是差值。

六  消费模式,消费者消费消息

MQ的消费模式可以大致分为两种,一种是推Push,一种是拉Pull。

Push是服务端【MQ】主动推送消息给客户端,优点是及时性较好,但如果客户端没有做好流控,一旦服务端推送大量消息到客户端时,就会导致客户端消息堆积甚至崩溃。

Pull是客户端需要主动到服务端取数据,优点是客户端可以依据自己的消费能力进行消费,但拉取的频率也需要用户自己控制,拉取频繁容易造成服务端和客户端的压力,拉取间隔长又容易造成消费不及时。

Push模式也是基于pull模式的,只能客户端内部封装了api,一般场景下,上游消息生产量小或者均速的时候,选择push模式。在特殊场景下,例如电商大促,抢优惠券等场景可以选择pull模式

在idea中,消费者有两种内用来创建,一种是push一种是pull,但是他们底层都封装的pull。

 // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
        DefaultMQPullConsumer defaultMQPullConsumer = new DefaultMQPullConsumer("consumer-group");

七  发送模式,生产者生产消息

1  同步消息

上面的快速入门就是发送同步消息,发送过后会有一个返回值,也就是mq服务器接收到消息后返回的一个确认,这种方式非常安全,但是性能上并没有这么高,而且在mq集群中,也是要等到所有的从机都复制了消息以后才会返回,所以针对重要的消息可以选择这种方式

SendResult send = producer.send(msg);

2  异步消息

异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。发送完以后会有一个异步消息通知。在这里”主线程执行“会比”发送成功“早出现

// 发送消息
        defaultMQProducer.send(message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("发送成功");
            }

            @Override
            public void onException(Throwable throwable) {
                System.out.println("发送失败");
            }
        });
        System.out.println("主线程执行");

3  单向消息

这种方式主要用在不关心发送结果的场景,这种方式吞吐量很大,但是存在消息丢失的风险,例如日志信息的发送。没有返回值。

package com.mq.learnrocketmq.demo;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.junit.Test;

public class OneWay {

    @Test
    public void textOneWay() throws Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("OneWay-group");
        // 设置nameServer地址
        producer.setNamesrvAddr("192.168.6.131:9876");
        // 启动实例
        producer.start();

        // 创建消息
        // 第一个参数:主题的名字
        // 第二个参数:消息内容
        Message msg = new Message("onewayTopic", "Hello RocketMQ ".getBytes());
        producer.sendOneway(msg);
        System.out.println("OK");

        // 关闭实例
        producer.shutdown();
    }
}

4  延迟消息

消息放入mq后,过一段时间,才会被监听到,然后消费

比如下订单业务,提交了一个订单就可以发送一个延时消息,30min后去检查这个订单的状态,如果还是未付款就取消订单释放库存。

这里注意的是RocketMQ不支持任意时间的延时

只支持以下几个固定的延时等级,等级1就对应1s,以此类推,最高支持2h延迟

private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

// 设置延迟时间
        // 给这个消息设定一个延迟等级
        // messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
        msg.setDelayTimeLevel(2);

        producer.send(msg);

5  批量消息

Rocketmq可以一次性发送一组消息,那么这一组消息会被当做一个消息消费

List<Message> msgs = Arrays.asList(
                new Message("TopicTest", "我是一组消息的A消息".getBytes()),
                new Message("TopicTest", "我是一组消息的B消息".getBytes()),
                new Message("TopicTest", "我是一组消息的C消息".getBytes())

        );
        SendResult send = producer.send(msgs);

这些消息会被分发到一个队列里面

6  顺序消息

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为:分区有序或者全局有序。

一个broker中对应了四个queue。

顺序消费的原理解析:在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。

但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

全局有序,生产者只会将消息发送到一个队列,而消费者只会从一个队列拉取消息。

分区有序,在默认情况下,生 产者将有某种特定联系的消息放在一个队列,不同联系的消息放在不同队列,一共有四个队列。消费者分别分四个线程单独拉取这四个队列的消息并处理。

下面用订单进行分区有序的示例。一个订单的顺序流程是:下订单、物流、签收。订单顺序号相同的消息会被先后发送到同一个队列中,消费时,同一个顺序获取到的肯定是同一个队列。

package com.mq.learnrocketmq.demo;

import com.mq.learnrocketmq.constant.AddrConstant;
import com.mq.learnrocketmq.domain.Order;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.junit.Test;

import java.util.Arrays;
import java.util.List;

public class Orderly {

    @Test
    public void testOrderlyProducer() throws Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("Orderly-group");
        // 设置nameServer地址
        producer.setNamesrvAddr(AddrConstant.NameSerAddr);
        // 启动实例
        producer.start();
        List<Order> orderlyList = Arrays.asList(
                new Order(1, "下单"),
                new Order(1, "物流"),
                new Order(1, "签收"),
                new Order(2, "下单"),
                new Order(2, "物流"),
                new Order(2, "签收")
        );

        orderlyList.forEach(order -> {
            Message message = new Message("orderTopic", order.toString().getBytes());
            try {
                producer.send(message, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message message, Object arg) {
                        // 当前主题有多少个队列
                        int queueNumber = mqs.size();
                        // 这个arg就是后面传入的 order.getOederId()
                        Integer i = (Integer) arg;
                        // 用这个值去%队列的个数得到一个队列
                        int index = i % queueNumber;
                        // 返回选择的这个队列即可 ,那么相同的订单号 就会被放在相同的队列里 实现FIFO了
                        return mqs.get(index);
                    }
                }, order.getOederId()/* 第三个参数传入订单号,根据订单号来选择队列, 传入到上面方法Object arg*/);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
        // 关闭实例
        producer.shutdown();
    }

    @Test
    public void testOrderlyConsumer() throws Exception {
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("Orderly-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr(AddrConstant.NameSerAddr);
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("orderTopic", "*");
        // 注册一个消费监听 MessageListenerOrderly 是顺序消费 单线程消费
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                MessageExt messageExt = msgs.get(0);
                System.out.println(new String(messageExt.getBody()));
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }
}

可以看到订单是按顺序排放的

八  发送带标签的消息,消息过滤

1  带tag

Rocketmq提供消息过滤功能,通过tag或者key进行区分

我们往一个主题里面发送消息的时候,根据业务逻辑,可能需要区分,比如带有tagA标签的被A消费,带有tagB标签的被B消费,还有在事务监听的类里面,只要是事务消息都要走同一个监听,我们也需要通过过滤才区别对待

下面演示,采用同步消息方式,定义了一个可以生产消息A和B的方法,还有一个只消费A  和 一个消费AB的消费者

相比较于没有tag的生产者生产消息,生产者new Message时,多传入一个参数tag,消费者订阅的时候,多设置过滤的条件。

package com.mq.learnrocketmq.demo;

import com.mq.learnrocketmq.constant.AddrConstant;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.junit.Test;

import java.util.List;

public class tag {

    @Test
    public void testTagProducer() throws Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("Tag-group");
        // 设置nameServer地址
        producer.setNamesrvAddr(AddrConstant.NameSerAddr);
        // 启动实例
        producer.start();
        Message msgA = new Message("TagTopic","tagA", "我是一个带A标记的消息".getBytes());
        Message msgB = new Message("TagTopic","tagB", "我是一个带B标记的消息".getBytes());
        SendResult sendA = producer.send(msgA);
        SendResult sendB = producer.send(msgB);

        System.out.println(sendA);
        System.out.println(sendB);
        // 关闭实例
        producer.shutdown();
    }


    // 只消费A
    @Test
    public void testTagConsumerA() throws Exception {
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr(AddrConstant.NameSerAddr);
        // 订阅一个主题来消费   表达式,默认是*,支持"tagA || tagB || tagC" 这样或者的写法 只要是符合任何一个标签都可以消费
        consumer.subscribe("TagTopic", "tagA");
        // 注册一个消费监听 MessageListenerConcurrently是并发消费
        // 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                // 这里执行消费的代码 默认是多线程消费
                System.out.println(Thread.currentThread().getName() + "----" + new String(msgs.get(0).getBody()));
                System.out.println(msgs.get(0).getTags());
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }

    // 消费A, B
    @Test
    public void testTagConsumerB() throws Exception {
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr(AddrConstant.NameSerAddr);
        // 订阅一个主题来消费   表达式,默认是*,支持"tagA || tagB" 这样或者的写法 只要是符合任何一个标签都可以消费
        consumer.subscribe("TagTopic", "tagA || tagB");
        // 注册一个消费监听 MessageListenerConcurrently是并发消费
        // 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                // 这里执行消费的代码 默认是多线程消费
                System.out.println(Thread.currentThread().getName() + "----" + new String(msgs.get(0).getBody()));
                System.out.println(msgs.get(0).getTags());
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }
}

消费者B

消费者A 

生产者

什么时候该用 Topic,什么时候该用 Tag?

总结:不同的业务应该使用不同的Topic如果是相同的业务里面有不同表的表现形式,那么我们要使用tag进行区分

可以从以下几个方面进行判断:

1.消息类型是否一致:如普通消息、事务消息、定时(延时)消息、顺序消息,不同的消息类型使用不同的 Topic,无法通过 Tag 进行区分。

2.业务是否相关联:没有直接关联的消息,如淘宝交易消息,京东物流消息使用不同的 Topic 进行区分;而同样是天猫交易消息,电器类订单、女装类订单、化妆品类订单的消息可以用 Tag 进行区分。

3.消息优先级是否一致:如同样是物流消息,盒马必须小时内送达,天猫超市 24 小时内送达,淘宝物流则相对会慢一些,不同优先级的消息用不同的 Topic 进行区分。

4.消息量级是否相当:有些业务消息虽然量小但是实时性要求高,如果跟某些万亿量级的消息使用同一个 Topic,则有可能会因为过长的等待时间而“饿死”,此时需要将不同量级的消息进行拆分,使用不同的 Topic。

总的来说,针对消息分类,您可以选择创建多个 Topic,或者在同一个 Topic 下创建多个 Tag。但通常情况下,不同的 Topic 之间的消息没有必然的联系,而 Tag 则用来区分同一个 Topic 下相互关联的消息,例如全集和子集的关系、流程先后的关系。

2  带key

在rocketmq中的消息,默认会有一个messageId当做消息的唯一标识,我们也可以给消息携带一个key,用作唯一标识或者业务标识,包括在控制面板查询的时候也可以使用messageId或者key来进行查询

其实相比于没有key的消息,在创建message的时候加入了key标识

Message msg = new Message("keyTest","tagA","key", "我是一个带标记和key的消息".getBytes());

在控制台也可以查询到

九  RocketMQ消息重复消费问题

1  消费重复问题的产生原因

1.1 生产者重复投递

在rocketMQ中生产者和broker通过tcp协议来连接。tcp连接经过三次握手四次挥手。

生产者生产了A消息后,经过第一次挥手发给了broker。如果这时连接中断,broker无法发送第二次挥手告诉生产者已经收到,那么生产者会将一样的消息传递给broker,从而造成重复投递。从而重复消费。

1.2  消费者方扩容,重平衡

一开始,我们消费者组只有c1是在工作的。他此时正在消费。队列2的消息A。此时由于c1还没返回消费成功给队列2,所以队列2的消费偏移点还在A上。

这个时候。c2来了并且要工作,此时MQ执行了rebalance(重平衡),由c1管理0,1队列,由c2管理2,3队列。此时c2就会消费消息A,从而造成了重复消费。

2  解决方案

那么如果在CLUSTERING(负载均衡)模式下,并且在同一个消费者组中,不希望一条消息被重复消费,改怎么办呢?我们可以想到去重操作,找到消息唯一的标识,可以是msgId也可以是你自定义的唯一的key,这样就可以去重了

使用去重方案解决,例如将消息的唯一标识存起来,然后每次消费之前先判断是否存在这个唯一标识,如果存在则不消费,如果不存在则消费,并且消费以后将这个标记保存

想法很好,但是消息的体量是非常大的,可能在生产环境中会到达上千万甚至上亿条,那么我们该如何选择一个容器来保存所有消息的标识,并且又可以快速的判断是否存在呢?

我们可以选择布隆过滤器(BloomFilter)

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。在hutool的工具中我们可以直接使用。

当然也可以自己写相关的逻辑。用mysql或者redis或者其他

引入hutool依赖

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.11</version>
</dependency>

下面我们来实现一下。生产者发送了两个一样的消息,消费者如果第一次遇到这个消息将会打印“不存在,执行操作”,之后再遇到将会打印“已经存在,不执行操作”。

package com.mq.learnrocketmq.demo;

import cn.hutool.bloomfilter.BitMapBloomFilter;
import com.mq.learnrocketmq.constant.AddrConstant;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.junit.Test;

import java.util.List;
import java.util.UUID;

public class DuplicateRemoval {


    public static BitMapBloomFilter bloomFilter = new BitMapBloomFilter(100);

    @Test
    public void testDuplicateRemovalProducer() throws Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("DuplicateRemoval-group");
        // 设置nameServer地址
        producer.setNamesrvAddr(AddrConstant.NameSerAddr);
        // 启动实例
        producer.start();

        String uuid = UUID.randomUUID().toString();
        
        Message msg1 = new Message("DuplicateRemovalTest", "tagA", uuid, "我是一个带标记和key的消息".getBytes());
        Message msg2 = new Message("DuplicateRemovalTest", "tagA", uuid, "我是一个带标记和key的消息".getBytes());

        SendResult send1 = producer.send(msg1);
        SendResult send2 = producer.send(msg2);
        System.out.println(send1);
        System.out.println(send2);
        // 关闭实例
        producer.shutdown();
    }


    @Test
    public void testKeyConsumer() throws Exception {
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("DuplicateRemoval-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr(AddrConstant.NameSerAddr);

        consumer.subscribe("DuplicateRemovalTest", "*");
        // 注册一个消费监听 MessageListenerConcurrently是并发消费
        // 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {

                // 拿到消息的key
                MessageExt messageExt = msgs.get(0);
                String keys = messageExt.getKeys();
                // 判断是否存在布隆过滤器中
                if (bloomFilter.contains(keys)) {
                    System.out.println("已经存在,不执行操作");
                    // 直接返回了 不往下处理业务
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
                // 这个处理业务,然后放入过滤器中
                // do sth...
                bloomFilter.add(keys);
                System.out.println("不存在,执行操作");
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }


}

十  消费者重试和死信

1  重试

1.1  生产者重试

默认的重复次数是2

// 默认是两次
producer.setRetryTimesWhenSendFailed(2);
producer.setRetryTimesWhenSendAsyncFailed(2);

1.2  消费者重试

在消费者放return ConsumeConcurrentlyStatus.RECONSUME_LATER;或者业务报错,或者返回null后就都会执行重试

默认并发模式重复16次,重复间隔:10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

顺序模式重复int的最大值次

下面我们测试一下。我们模拟一直重试

@Test
    public void testConsumer() throws Exception {
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr(AddrConstant.NameSerAddr);
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("retryTest", "*");
        // 注册一个消费监听
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                MessageExt messageExt = msgs.get(0);
                System.out.println(messageExt);
                System.out.println(new Date());

                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        });
        consumer.start();
        System.in.read();
    }

重复间隔显示

我们可以自定义重复次数

消息由消息体和消息头组成,重试次数在消息头

在消费者放return ConsumeConcurrentlyStatus.RECONSUME_LATER;,或者业务报错后就会执行重试

上图代码中说明了,我们再实际生产过程中,一般重试3-5次,如果还没有消费成功,则可以把消息签收了,通知人工等处理

@Test
public void testConsumer() throws Exception {
    // 创建默认消费者组
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
    // 设置nameServer地址
    consumer.setNamesrvAddr("localhost:9876");
    // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
    consumer.subscribe("TopicTest", "*");
    // 注册一个消费监听
    consumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                        ConsumeConcurrentlyContext context) {
            try {
                // 这里执行消费的代码
                System.out.println(Thread.currentThread().getName() + "----" + msgs);
                // 这里制造一个错误
                int i = 10 / 0;
            } catch (Exception e) {
                // 出现问题 判断重试的次数
                MessageExt messageExt = msgs.get(0);
                // 获取重试的次数 失败一次消息中的失败次数会累加一次
                int reconsumeTimes = messageExt.getReconsumeTimes();
                if (reconsumeTimes >= 3) {
                    // 则把消息确认了,可以将这条消息记录到日志或者数据库 通知人工处理
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                } else {
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
    consumer.start();
    System.in.read();
}

2  死信

当消费重试到达阈值以后,消息不会被投递给消费者了,而是进入了死信队列

当一条消息初次消费失败,RocketMQ会自动进行消息重试,达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息。

此时,该消息不会立刻被丢弃,而是将其发送到该消费者对应的特殊队列中,这类消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue),死信队列是死信Topic下分区数唯一的单独队列。

如果产生了死信消息,那对应的(ConsumerGroup)的死信Topic名称为%DLQ%(ConsumerGroupName),死信队列的消息将不会再被消费。可以利用RocketMQ Admin工具或者RocketMQ Dashboard上查询到对应死信消息的信息。我们也可以去监听死信队列,然后进行自己的业务上的逻辑

十一   boot集成RocketMQ

1  快速入门

1.1  springboot依赖

我们引入了2.7.12版本的springboot,rocketMQ-start 2.2.2版本及其他配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.MQ</groupId>
    <artifactId>RocketMQProductor</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>RocketMQProductor</name>
    <description>RocketMQProductor</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


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

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>




	</dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

1.2  配置信息

必要信息是nameServer的地址和生产者组别。其他可选择性配置

spring:
  application:
    name: rocketmq-producer
rocketmq:
  name-server: 192.168.6.132:9876     # rocketMq的nameServer地址
  producer:
    group: powernode-group        # 生产者组别
    send-message-timeout: 3000  # 消息发送的超时时间
    retry-times-when-send-async-failed: 2  # 异步消息发送失败重试次数
    max-message-size: 4194304       # 消息的最大长度

1.3  注入RocketMQ

定义一个配置类来注入RocketMQ

package com.mq.rocketmqproductor.config;


import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MQConfig {

    @Bean
    public RocketMQTemplate makeRocketMQ(){
        return new RocketMQTemplate();
    }
}

1.4  生产者

使用rocketMQ的bean。使用同步发消息方法,第一个参数是主题,第二个是消息内容

	@Autowired
	private RocketMQTemplate rocketMQTemplate;

	@Test
	public void testSimpleMsgP(){
		SendResult sendResult = rocketMQTemplate.syncSend("powernode", "cjx是帅哥");
		System.out.println(sendResult.getSendStatus());
		System.out.println(sendResult.getMsgId());
	}

1.5  消费者

一个boot可以设置多个消费者,但是一般建议只设置一个消费者。

配置监听器,用  @RocketMQMessageListener  注解,并定义监听的主题和生产者组的名字。

要得到消息,就要继承监听者类,这个类将消息的类型形式转化为了泛型的类型。当然这个泛型可以自己定义。MessageExt包括了消息头和消息题,如果是String类型,就只有消息体。

这个方法的返回值是void,那么我们如何知道消息发送成功与否呢?

我们只需要看这个方法有没有报错就可以了,没有报错就接收,报错了就是拒收,重试

package com.mq.rocketmqproductor.listener;


import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

@Component
@RocketMQMessageListener(topic = "powernode", consumerGroup = "bootMQC")
public class MQListen implements RocketMQListener<MessageExt> {


    @Override
    public void onMessage(MessageExt messageExt) {

        System.out.println(new String(messageExt.getBody()));

    }
}

2  消息类型

2.1  同步消息

快速入门就是同步消息

2.2  单向消息

第一个参数是设置主题,第二个是设置负载,也就是消息的内容

// 单向消息
	@Test
	public void testOneWayMsgP(){
		rocketMQTemplate.sendOneWay("OneWayTopic", "单向消息");
	}

2.3  延迟消息

第一个参数设置发送主题,第二个是发送的消息, 第三个是timeout: 发送消息的超时时间,单位为毫秒。如果在指定的时间内未能成功发送消息,则会抛出 RocketMQException 异常,第四个是延迟的等级。

这里注意的是RocketMQ不支持任意时间的延时

只支持以下几个固定的延时等级,等级1就对应1s,以此类推,最高支持2h延迟

private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

	//延迟消息
	@Test
	public void testDelayMsgP(){
		Message<String> oneDelayMessage = MessageBuilder.withPayload("one delay message").build();
		rocketMQTemplate.syncSend("delayMessage", oneDelayMessage,
				3000, 2);
		Date date = new Date();
		System.out.println(date);
	}

2.4  顺序消息生产者和消费者

生产者:

设置一个实体类来演示

package com.mq.rocketmqproductor.domain;


import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Order {

    private Integer orderId;
    private String desc;
}

测试顺序发送方法。

第一个参数是发送的主题,第二个是消息对象,第三个是放入同一个队列中的凭证字段。在前面分区有序中有讲解。

@Test
	public void testOrderlyProducer(){
		List<Order> orderlyList = Arrays.asList(
				new Order(1, "下单"),
				new Order(1, "物流"),
				new Order(1, "签收"),
				new Order(2, "下单"),
				new Order(2, "物流"),
				new Order(2, "签收")
		);
		rocketMQTemplate.syncSendOrderly("OrderMsg", orderlyList, "orderid");

	}

消费者:

在注解上和其他不同,会多一个消费模式。由于我们顺序消息需要单线程来消费,所以我们要设置消费模式为顺序消费,此模式就是单线程消费。默认是并发消费,并发消费是多线程的。

package com.mq.rocketmqproductor.listener;


import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

@Component
@RocketMQMessageListener(topic = "OrderMsg",
                        consumerGroup = "order-group",
                        consumeMode = ConsumeMode.ORDERLY)
public class OrderlyListen implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt messageExt) {
        byte[] body = messageExt.getBody();
        System.out.println(new String(body));
    }
}

3  发送带标签的消息

3.1  带tag

生产者

语法:就是在topic加上 “:” 来分割,后面跟着tag

@Test
	public void testTag(){
		rocketMQTemplate.syncSend("tagTopic:tagA", "我是一条带tag的消息");
	}

消费者

比其他在注解上多了两个参数。selectTypr,顾名思义就是选择的类型,这里选择tag,也是默认的选择。还有一个是SQL92,基本用不到。selectorExpression就是过滤tag,对应着原始的comsumer.subscribe()方法。

package com.mq.rocketmqproductor.listener;


import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.annotation.SelectorType;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

@Component
@RocketMQMessageListener(topic = "tagTopic",
                        consumerGroup = "tagTopic-grouo",
                        selectorType = SelectorType.TAG,
                        selectorExpression = "tagA")

public class TagListen implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt messageExt) {
        byte[] body = messageExt.getBody();
        System.out.println(new String(body));
    }
}

3.2  带key

将key设置在消息头处。

@Test
	public void testKey(){
		Message<String> message = MessageBuilder.withPayload("我是一条带key的消息").
				setHeader(RocketMQHeaders.KEYS, "java").build();
		rocketMQTemplate.syncSend("keyTopic", message);
	}

4   RocketMQ集成SpringBoot消息消费两种模式 

Rocketmq消息消费的模式分为两种:负载均衡模式和广播模式

负载均衡模式表示多个消费者交替消费同一个主题里面的消息

广播模式表示每个每个消费者都消费一遍订阅的主题的消息

4.1  特点

CLUSTERING: 集群模式下

1  队列会被消费者分摊

2  队列数量>=消费者数量

3  消息的消费位点会被记录

4  mq服务器会记录处理

BROADCASTING :广播模式下

1  消息会被每一个消费者都处理一次

2  mg服务器不会记录消费点位

3  也不会重试

4.2  负载均衡模式

在消费者的注解中加入messageModel这个参数。CLUSTERING是负载均衡

@RocketMQMessageListener(topic = "powernode",
        consumerGroup = "powernode-group",
        messageModel = MessageModel.CLUSTERING
)

4.3  广播模式

BROADCASTING 为广播模式

@RocketMQMessageListener(topic = "powernode",
        consumerGroup = "powernode-group",
        messageModel = MessageModel.BROADCASTING 
)

十二  如何解决消息堆积问题

1  堆积消息是什么

一般认为单条队列消息差值>=10w时 算堆积问题

2  堆积出现原因及解决方法

原因一:

生产太快了

对应方法

1  生产方可以做业务限流

2  增加消费者数量,但是消费者数量<=队列数量,适当的设置最大的消费线程数量(根据  读写IO(2n)/  计算  CPU(n+1))

可以通过java程序来获得线程数量

int i = Runtime.getRuntime().availableProcessors();

可以在电脑任务管理器中看到逻辑处理器,就是线程数。如果是io操作的话就设置线程数是2n

通过消费者中的监听注解,里面的参数consumeThreadNumber 来设置最大线程数。默认是20.

由于我电脑处理器是16核,所以我设置2 * 16 = 32

@RocketMQMessageListener(topic = "tagTopic",
                        consumerGroup = "tagTopic-grouo",
                        consumeThreadNumber = 32)

3  动态扩容队列数量,从而增加消费者数量

一般来说修改队列数量不是由开发者决定,而是由运维决定。

一般的队列读写数要保持一致,数量为4, 6, 8

.下面的perm是权限。2表示只写, 4表示只读, 6表示可读写

注意:扩容可以不用在意差值,缩容要等到差值为0才能缩,以防消息丢失。

原因二:

消费者消费出现问题

对应方法

排查消费者程序的问题。当我们不需要堆积的消息,我们可以在面板删除指定的消费者的堆积消息。点击重置消费位点,选择消费者组,点击重置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值