RocketMQ学习笔记(1)—— 基础使用

3 篇文章 0 订阅

1.概述

如何理解MQ:Message Queue 即消息队列

消息:即数据;队列:是一种数据结构,起到存储消息的作用

官方文档:为什么选择RocketMQ | RocketMQ

1.1 RocketMQ简介

RocketMQ是由阿里巴巴2016年开发的MQ中间件,使用Java语言开发;RocketMQ 是一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务;同时广泛应用于多个领域,包括异步通信解耦、企业解决方案、金融支付、电信、电子商务、快递物流、广告营销、社交、即时通信、移动应用、手游、视频、物联网、车联网等;具有以下特点:

1.能够保证严格的消息顺序

2.提供丰富的消息拉取模式

3.高效的订阅者水平扩展能力

4.实时的消息订阅机制

5.亿级消息堆积能力

1.2 为什么要使用MQ

1.削峰限流:

如果不使用mq,当有大量请求发送到服务器时,会对请求逐个进行处理,然后返回结果,这样会对后端系统造成压力,并且请求响应不及时;而mq的使用可以起到很好的缓冲作用,mq接收到大量请求,会正常返回响应,然后让后端系统按自身吞吐能力进行消费,不被冲垮

2.解耦合:消息队列允许生产者系统将消息发送到队列中,而不需要等待消费者系统立即处理。这样,生产者系统可以继续执行其他任务,而不必关心消费者系统的处理速度和状态;也就是说,生产者和消费者不必了解对方,只需要确认消息,并且发送者和接收者不必同时在线

这样可以取消应用之间/模块和模块之间的强关系,使得程序更加健壮,提高高可用性

3.异步处理:消息发送者可以发送一个消息而无需等待响应;消息发送者把消息发送到一条虚拟的通道(主题或队列)上;

消息接收者则订阅或监听该通道;一条信息可能最终转发给一个或多个消息接收者,这些接收者都无需对消息发送者做出回应,整个过程都是异步的

1.3 各个MQ产品的比较

  • 吞吐量:指的是单位时间内接收和处理消息的速度
  • rocketmq具有很好的跨平台性,即生产者和消费者可以运行在不同的平台上,而通过mq进行消息的传递

2.重要概念

  1. Producer:消息的发送者,生产者;举例:发件人
  2. Consumer:消息接收者,消费者;举例:收件人
  3. Broker:暂存和传输消息的通道(代理服务器);举例:快递
  4. NameServer:管理Broker,相当于broker的注册中心,保留了broker的信息;举例:各个快递公司的管理机构
  5. Queue:队列,消息存放的位置,一个Broker中可以有多个队列
  6. Topic:主题,消息的分类(是一个逻辑结构)
  7. ProducerGroup:生产者组
  8. ConsumerGroup:消费者组,多个消费者组可以同时消费一个主题的消息

2.1 消息模型

RocketMQ的基础消息模型是一个简单的Pub/Sub模型

发布-订阅(Pub/Sub)是一种消息范式,消息的发送者(称为发布者、生产者、Producer)会将消息直接发送给特定的接收者(称为订阅者、消费者、Comsumer)

生产者和消费者基于消息主题(topic)进行消息的传递

扩展后的消息模型:

2.2 部署架构

主要分为producer,consumer,nameserver,broker四部分;

生产者 Producer

发布消息的角色;Producer通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败和重试

消费者 Consumer

消息消费的角色

  • 支持以推(push),拉(pull)两种模式对消息进行消费(底层上都是pull)
  • 同时也支持集群方式(负载均衡)广播方式的消费(topic内的每一条消息会投递给所有订阅该topic的消费者组,具体的消费方式是在消费者组中进行配置的)
  • 提供实时消息订阅机制,可以满足大多数用户的需求

集群方式和广播方式的区别:
集群方式:topic各队列中的数据通过轮询均匀地分配给所有消费者

广播方式:topic中所有数据给每一个消费者都发送一份

NameServer

NameServer是一个简单的 Topic 路由注册中心,支持 Topic、Broker 的动态注册与发现

主要包括两个功能:

  1. Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活
  2. 路由信息管理,每个NameServer将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息。Producer和Consumer通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费

代理服务器 Broker

Broker主要负责消息的存储、投递和查询以及服务高可用保证

Broker支持Master-Slave(主从)架构,以此来实现高可用:

在 Master-Slave 架构中,Broker 分为 Master 与 Slave。一个Master可以对应多个Slave,但是一个Slave只能对应一个Master。Master 与 Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId 来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个

部署架构总结*

  1. 每个 Broker 与 NameServer 集群中的所有节点建立长连接,定时注册 Topic 信息到所有 NameServer
  2. Producer 与 NameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取Topic路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master 发送心跳。Producer 完全无状态。
  3. Consumer 与 NameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave发送心跳。Consumer 既可以从 Master 订阅消息,也可以从Slave订阅消息

2.3 集群工作流程

  1. 启动NameServer
  2. 启动Broker
  3. 创建Topic
  4. 生产者发送消息
  5. 消费者接收消息

2.4 消息发送流程

  1. Producer询问NameServer,NameServer分配一个broker
  2. Producer向broker中的某个topic发送消息
  3. Consumer也要询问NameServer,得到一个具体的broker
  4. Consumer消费broker相应topic中的消息

3.RocketMQ安装

3.1 下载

下载地址:下载 | RocketMQ

这里安装4.9.2版本

3.2 上传并解压

建议安装在linux服务器上;

首先上传rocketmq的安装包到服务器上,然后使用unzip进行解压:

unzip rocketmq-all-4.9.2-bin-release.zip -d /opt/module/

解压完成:

Benchmark:包含一些性能测试的脚本;

Bin:可执行文件目录;

Conf:配置文件目录;

Lib:第三方依赖;

LICENSE:授权信息;

NOTICE:版本公告;

3.3 配置环境变量

修改环境变量文件:vim /etc/profile

在文件末尾添加NameServer的IP地址:export NAMESRV_ADDR=IP:9876

如果是云服务器,需要填写公网IP

然后使环境变量失效:source /etc/profile

3.4 修改nameServer的运行脚本

进入bin目录下,修改runserver.sh文件,将的Xms和Xmx等改小一点

修改时等比缩放即可;

Xms:表示JVM启动时的初始堆大小(Initial Heap Size)。它是JVM启动时分配的内存量,这个值可以根据应用程序的需求来设置,以确保应用程序在启动时有足够的内存可用。设置较大的Xms可以使程序更快地启动,但也可能会占用更多的系统资源。

Xmx:表示JVM运行期间最大可占用的堆大小(Maximum Heap Size)。这是JVM可以使用的最大内存量,超过这个值JVM将抛出OutOfMemoryError。合理设置Xmx可以避免因内存溢出而导致的程序崩溃,但也不能设置得太大,以免影响系统的稳定性和其他应用程序的运行。

Xmn:表示JVM中新生代的大小(Young Generation Size)。新生代是堆内存的一部分,用于存放新创建的对象。合理配置新生代的大小对于垃圾回收器的性能至关重要,因为大部分的对象回收都在新生代进行。Xmn的值通常需要根据应用程序的特点和垃圾回收器的行为来调整。

3.5 修改broker的运行脚本

进入bin目录下,修改runbroker.sh文件:

3.6 修改broker的配置文件

进入conf目录下,修改broker.conf文件,添加以下内容:

namesrvAddr=localhost:9876
autoCreateTopicEnable=true
brokerIP1=hadoop104

参数解析:

  • namesrvAddr:nameSrv地址 可以写localhost,因为nameSrv和broker在一个服务器
  • autoCreateTopicEnable:自动创建主题(这样生产者直接向topic中发送数据即可,topic会自动创建,不然需要手动创建出来)
  • brokerIP1:broker的ip

3.7 启动

  1. 启动NameServer:nohup sh bin/mqnamesrv > ./logs/namesrv.log &
  2. 启动Broker:nohup sh bin/mqbroker -c conf/broker.conf > ./logs/broker.log &

启动命令解析:

在mqnamesrv中:

最后执行了runserver.sh

mqbroker也是同理,执行了runbroker.sh

3.8 RocketMQ控制台安装(RocketMQ-Console)

Rocketmq 控制台可以可视化MQ的消息发送

网址:GitHub - apache/rocketmq-dashboard: The state-of-the-art Dashboard of Apache RoccketMQ provides excellent monitoring capability. Various graphs and statistics of events, performance and system information of clients and application is evidently made available to the user.

下载地址:https://github.com/apache/rocketmq-dashboard/archive/refs/tags/rocketmq-dashboard-1.0.0.zip

下载完成后进行打包,在根目录下执行:mvn clean package -Dmaven.test.skip=true

然后将jar包上传到服务器上去:

运行jar包即可启动:

nohup java -jar /opt/software/rocketmq-dashboard-1.0.0.jar --rocketmq.config.namesrvAddr=hadoop104:9876 --server.port=8001 > /opt/module/rocketmq-4.9.2/logs/dashboard.log &

通过--rocketmq.config.namesrvAddr指定namesrv地址

通过--server.port指定端口号,默认是8080

运行之后打开网页:http://hadoop104:8001/

4.简单入门

实现一个消息发送和接收的简单demo,流程如下:
 

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

2.指定Nameserver地址

3.启动producer

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

5.发送消息

6.关闭生产者producer

消费者:

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

2.指定Nameserver地址

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

4.处理消息

5.启动消费者consumer

4.1 环境搭建

创建springboot module,添加Spring Web和Lombok组件,然后在pom文件中添加:

<!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-client -->
<dependency>
  <groupId>org.apache.rocketmq</groupId>
  <artifactId>rocketmq-client</artifactId>
  <version>4.9.2</version>
</dependency>

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

4.2 生产者

//生产者
    @Test
    public void simpleProducer() throws Exception {
        //1.创建生产者
        DefaultMQProducer producer = new DefaultMQProducer("test-producer-group");

        //2.指定NameServer地址
        producer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        //3.启动producer
        producer.start();

        //4.创建消息对象
        Message message = new Message("testTopic", "一条简单的消息".getBytes());

        //5.发送消息
        producer.send(message);

        System.out.println("消息发送成功");

        //6.关闭生产者
        producer.shutdown();
    }

4.3 消费者

代码

//消费者
    @Test
    public void simpleConsumer() throws Exception{
        //1.创建消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-consumer-group");

        //2.指定NameServer地址
        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        //3.订阅主题
        consumer.subscribe("testTopic","*");

        //4.创建监听器(监听方法是异步执行的)
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            //list 消息对象,包括消息头和消息体
            //consumeConcurrentlyContext 上下文对象
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                String msg = new String(list.get(0).getBody());
                System.out.println("收到消息:" + msg);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        //4.启动消费者
        consumer.start();

        //5.挂起消费者
        System.in.read();
    }

消费模式

对于消费者来说,有两种消费模式,push和pull(DefaultMQPushConsumerDefaultMQPullConsumer

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

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

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

运行结果

生产者成功发送消息:

消费者收到消息:

在dashboard中可以看到:

生产者将一条消息发送到了topic testTopic的消息队列2中,代理者位点(偏移量)+1

消费者组test-consumer-group订阅了testTopic,并成功消费了其中的消息,消费者位点+1

5.RocketMQ发送消息

5.1 发送同步消息

见4.简单入门案例

5.2 发送异步消息

重点

生产者发送异步消息和同步消息均使用send方法,不同的是异步消息多出一个参数:SendCallback


其中定义了两个方法:onSuccessonException

分别是消息发送成功或失败时调用

代码

public class BAsyncTest {
    //生产者
    @Test
    public void asyncProducer() throws Exception {

        //1.创建生产者
        DefaultMQProducer producer = new DefaultMQProducer("async-producer-group");

        //2.指定NameServer地址
        producer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        //3.启动producer
        producer.start();

        //4.创建消息对象
        Message message = new Message("asyncTopic", "一条异步发送的消息".getBytes());

        //5.异步发送消息
        producer.send(message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("消息发送成功:" + sendResult.getSendStatus());
            }

            @Override
            public void onException(Throwable throwable) {
                System.out.println("消息发送失败:" + throwable.toString());
            }
        });

        System.out.println("我先执行");

        //挂起生产者,否则会因为消息异步执行而无法测试
        System.in.read();

        //6.关闭生产者
        producer.shutdown();
    }

    //消费者
    @Test
    public void asyncConsumer() throws Exception{
        //1.创建消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("async-consumer-group");

        //2.指定NameServer地址
        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        //3.订阅主题
        consumer.subscribe("asyncTopic","*");

        //4.创建监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                String msg = new String(list.get(0).getBody());
                System.out.println("收到消息:" + msg);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        //4.启动消费者
        consumer.start();

        //5.挂起消费者
        System.in.read();
    }

}

运行结果

生产者:

消费者:

成功消费到消息


 

5.3 发送单向消息

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

重点

通过sendOneway方法来发送消息:

代码

public class COnewayTest {
    //生产者
    @Test
    public void onewayProducer() throws Exception {

        //1.创建生产者
        DefaultMQProducer producer = new DefaultMQProducer("oneway-producer-group");

        //2.指定NameServer地址
        producer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        //3.启动producer
        producer.start();

        //4.创建消息对象
        Message message = new Message("onewayTopic", "一条单向消息".getBytes());

        //5.发送消息
        producer.sendOneway(message);

        //6.关闭生产者
        producer.shutdown();
    }

    //消费者
    @Test
    public void onewayConsumer() throws Exception{
        //1.创建消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("async-consumer-group");

        //2.指定NameServer地址
        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        //3.订阅主题
        consumer.subscribe("onewayTopic","*");

        //4.创建监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                String msg = new String(list.get(0).getBody());
                System.out.println("收到单向消息:" + msg);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        //4.启动消费者
        consumer.start();

        //5.挂起消费者
        System.in.read();
    }

}

5.4 发送延迟消息

所谓延迟消息,就是指消息被放入MQ之后,过一段时间才能被消费者取出消费;

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

重点

创建完消息对象后,通过message.setDelayTimeLevel方法来设置延时时间:

延时时间一共有18个等级,对于的时间分别为:messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

setDelayTimeLevel函数中只需指定其延时等级即可

如果需要自定义延迟等级对应的时间,在broker.conf配置文件中设置messageDelayLevel的值即可,格式如下:

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

代码

public class DMsTest {

    //生产者
    @Test
    public void msProducer() throws Exception {

        //1.创建生产者
        DefaultMQProducer producer = new DefaultMQProducer("ms-producer-group");

        //2.指定NameServer地址
        producer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        //3.启动producer
        producer.start();

        //4.创建消息对象
        Message message = new Message("msTopic", "一条延时消息".getBytes());

        //设置延迟时间
        message.setDelayTimeLevel(3);

        //5.发送消息
        producer.send(message);
        System.out.println("消息发送时间:" + new Date());

        //6.关闭生产者
        producer.shutdown();
    }

    //消费者
    @Test
    public void msConsumer() throws Exception{
        //1.创建消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ms-consumer-group");

        //2.指定NameServer地址
        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        //3.订阅主题
        consumer.subscribe("msTopic","*");

        //4.创建监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                String msg = new String(list.get(0).getBody());
                System.out.println("收到消息:" + msg + ",消息收到时间:" + new Date());
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        //4.启动消费者
        consumer.start();

        //5.挂起消费者
        System.in.read();
    }
}

运行结果

设置的延时等级为3,对应的延时时间为10s

需要注意的时消息的首次发送与接收会因为生产者/消费者的启动而导致一定的误差

5.5 发送顺序消息*

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

在默认的情况下,是不能保证消息有序消费的,这跟rocketmq的broker机制有关;

默认情况下,一个broker中有4个队列;消息发送时会采取Round Robin轮询方式把消息发送到不同的分区队列中;而消费消息的时候从多个queue上拉取消息,这种情况下无法保证从不同的队列中拉取的数据消费时的顺序和消息发送时的顺序一致;但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,就可以保证顺序一致;

当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的

应用场景

以订单场景为例,一个订单的顺序流程是:下订单、发短信通知、物流、签收。订单顺序号相同的消息会被先后发送到同一个队列中,消费时,同一个顺序获取到的肯定是同一个队列

为了模拟场景,我们需要创建一个订单对象,然后模拟生成两条订单数据:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    /**
     * 订单id
     */
    private Integer orderId;

    /**
     * 订单编号
     */
    private Integer orderNumber;
    
    /**
     * 订单价格
     */
    private Double price;

    /**
     * 订单号创建时间
     */
    private Date createTime;

    /**
     * 订单描述
     */
    private String desc;

}

注解含义如下:

  1. @Data:这个注解会自动为类生成getter、setter、equals、hashCode和toString方法。它相当于同时使用了@Getter、@Setter、@ToString、@EqualsAndHashCode这四个注解。
  2. @AllArgsConstructor:这个注解会自动为类生成一个全参数构造器。也就是说,如果你的类有多个属性,那么这个注解会生成一个包含所有属性的构造函数。
  3. @NoArgsConstructor:这个注解会自动为类生成一个无参构造器。也就是说,如果你的类有多个属性,那么这个注解会生成一个不包含任何参数的构造函数。

重点

1.send方法解析:

2.消费时应当选取单线程顺序消费:

否则会因为并发导致消费的顺序和生产者生成消息的顺序不一致;

代码

public class EOrderlyTest {

    private List<Order> orderLists = new ArrayList<>();

    @Test
    public void orderlyProducer() throws Exception {
        //初始化订单对象
        orderLists = Arrays.asList(
                new Order(1, 111, 59D, new Date(), "下订单"),
                new Order(2, 111, 59D, new Date(), "物流"),
                new Order(3, 111, 59D, new Date(), "签收"),
                new Order(4, 112, 89D, new Date(), "下订单"),
                new Order(5, 112, 89D, new Date(), "物流"),
                new Order(6, 112, 89D, new Date(), "拒收")
        );

        DefaultMQProducer producer = new DefaultMQProducer("orderly-producer-group");

        producer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        producer.start();

        orderLists.forEach(order -> {
            Message message = new Message("orderlyTopic", order.toString().getBytes());

            try {
                producer.send(message, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message message, Object o) {
                        int size = mqs.size();
                        int index = (Integer) o % size;
                        return mqs.get(index);
                    }
                }, order.getOrderNumber());
            } catch (Exception e) {
                System.out.println("顺序消息发送错误");
            }
        });

        producer.shutdown();
    }


    @Test
    public void orderlyConsumer() throws Exception{
        //1.创建消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderly-consumer-group");

        //2.指定NameServer地址
        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        //3.订阅主题
        consumer.subscribe("orderlyTopic","*");

        //4.创建监听器
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
                String msg = new String(list.get(0).getBody());
                System.out.println("收到消息:" + msg);
                return ConsumeOrderlyStatus.SUCCESS;
            }

        });

        //4.启动消费者
        consumer.start();

        //5.挂起消费者
        System.in.read();
    }
}

运行结果

可以看到,按照生产者发送消息的顺序进行了消费

在控制面板可以看到:

订单号相同的数据均发送到了同一个队列中

5.6 发送批量消息

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

重点

与普通消息的发送一致,可以创建一个Message类型的List,然后通过send方法一并发送List中的所有数据

代码

public class FBatchTest {
    //生产者
    @Test
    public void msProducer() throws Exception {

        //1.创建生产者
        DefaultMQProducer producer = new DefaultMQProducer("batch-producer-group");

        //2.指定NameServer地址
        producer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        //3.启动producer
        producer.start();


        //4.创建消息对象
        List<Message> messageList = Arrays.asList(
                new Message("batchTopic", "message A".getBytes()),
                new Message("batchTopic", "message B".getBytes()),
                new Message("batchTopic", "message C".getBytes())
        );

        //5.发送消息
        SendResult sendResult = producer.send(messageList);
        System.out.println(sendResult);

        //6.关闭生产者
        producer.shutdown();
    }

    //消费者
    @Test
    public void msConsumer() throws Exception{
        //1.创建消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("batch-consumer-group");

        //2.指定NameServer地址
        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        //3.订阅主题
        consumer.subscribe("batchTopic","*");

        //4.创建监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                String msg = new String(list.get(0).getBody());
                System.out.println("收到消息:" + msg);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        //4.启动消费者
        consumer.start();

        //5.挂起消费者
        System.in.read();
    }
}

运行结果

可以看到,同一批次的消息被发送到了同一个消息队列中去

5.7 发送带标签的消息

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

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

重点

1.如何添加标签:为消息添加标签,在创建消息对象时填写标签参数即可;

2.消费者如何根据标签确定要消费的消息:

在订阅topic时指定tag即可,例如:consumer.subscribe("tagsTopic","tagA || tagB");

tagA || tagB就表示tagA及tagB

代码

public class GTagTest {
    //生产者
    @Test
    public void tagsProducer() throws Exception {

        DefaultMQProducer producer = new DefaultMQProducer("tags-producer-group");

        producer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        producer.start();

        Message messageA = new Message("tagsTopic", "tagA", "我是属于tagA的消息".getBytes());
        Message messageB = new Message("tagsTopic", "tagB", "我是属于tagB的消息".getBytes());

        producer.send(messageA);
        producer.send(messageB);

        producer.shutdown();
    }

    //消费来自tagA的消息
    @Test
    public void tagAConsumer() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("tags-consumer-group");

        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        consumer.subscribe("tagsTopic","tagA");

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                String msg = new String(list.get(0).getBody());
                System.out.println("收到来自tagA的消息:" + msg);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();

        System.in.read();
    }

    //消费来自tagA || tagB的消息
    @Test
    public void tagABConsumer() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("tags-consumer-group");

        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        consumer.subscribe("tagsTopic","tagA || tagB");

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                String msg = new String(list.get(0).getBody());
                System.out.println("收到来自tagA和tagB的消息:" + msg);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();

        System.in.read();
    }
}

运行结果

启动消费者tagAConsumer时:

启动消费者tagABConsumer时:

使用场景*

需要明确,什么时候使用topic,什么时候使用tag;

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

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

  1. 消息类型是否一致:如普通消息、事务消息、定时(延时)消息、顺序消息,不同的消息类型使用不同的 Topic,无法通过 Tag 进行区分
  2. 业务是否相关联:没有直接关联的消息,如淘宝交易消息,京东物流消息使用不同的 Topic 进行区分;而同样是天猫交易消息,电器类订单、女装类订单、化妆品类订单的消息可以用 Tag 进行区分
  3. 消息优先级是否一致:如同样是物流消息,盒马必须小时内送达,天猫超市 24 小时内送达,淘宝物流则相对会慢一些,不同优先级的消息用不同的 Topic 进行区分
  4. 消息量级是否相当:有些业务消息虽然量小但是实时性要求高,如果跟某些万亿量级的消息使用同一个 Topic,则有可能会因为过长的等待时间而“饿死”,此时需要将不同量级的消息进行拆分,使用不同的 Topic。

总的来说,针对消息分类,可以选择创建多个 Topic,或者在同一个 Topic 下创建多个 Tag

但通常情况下,不同的 Topic 之间的消息没有必然的联系,而 Tag 则用来区分同一个 Topic 下相互关联的消息,例如全集和子集的关系、流程先后的关系

5.8 发送带key的消息

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

messageId的使用

在控制面板中,可以通过topic,key和message ID来查找消息:

可以通过message ID来查看消息的具体内容:

重点

1.如何设置key:

需要注意:key一般使用业务参数,需要确保唯一;

代码

public class HKeyTest {

    @Test
    public void keyProducer() throws Exception {

        DefaultMQProducer producer = new DefaultMQProducer("key-producer-group");

        producer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        producer.start();

        //key要确保是唯一的
        String key = UUID.randomUUID().toString();
        Message message = new Message("keyTopic", "tagA",key, ("我的唯一标识是:" + key).getBytes());

        producer.send(message);
        producer.shutdown();
    }

    @Test
    public void keyConsumer() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("key-consumer-group");

        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        consumer.subscribe("keyTopic","*");

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messageExts, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                String msg = new String(messageExts.get(0).getBody());
                System.out.println("收到消息:" + msg);
                System.out.println("消息的唯一标识是:" + messageExts.get(0).getKeys());
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();

        System.in.read();
    }

}

运行结果

可以看到,从消息头中可以取出其唯一标识key:

6.RocketMQ重点问题

6.1 RocketMQ重试机制&死信消息

生产者重试机制

在生产者发送消息时可以设置重试次数:

// 重试次数
producer.setRetryTimesWhenSendFailed(2);

// 异步发送消息失败时的重试次数
producer.setRetryTimesWhenSendAsyncFailed(2)

完整代码如下:

 public void retryProducer() throws Exception {

        DefaultMQProducer producer = new DefaultMQProducer("retry-producer-group");

        producer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        producer.start();
        //设置重试次数
        producer.setRetryTimesWhenSendFailed(2);
        producer.setRetryTimesWhenSendAsyncFailed(2);

        Message message = new Message("retryTopic", "重试机制测试".getBytes());



        producer.send(message);
        producer.shutdown();
    }

消费者重试机制

可以通过consumer.setMaxReconsumeTimes(2);来设置重试次数;

重试的时间间隔如下:10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h(16个等级)

和延迟消息发送的时间间隔一样

比如说设置consumer.setMaxReconsumeTimes(2);意思就是如果消费者没有正常接收到消息,则10s后重新尝试接收消息,如果还没有接收到,则30s后再次尝试接收消息,如果仍然没有接收到,则进行其他处理;

在并行模式下,最多可以重试的次数为16次,在顺序模式下,最多可以重试的次数为int的最大值,如果达到了设置的重试次数或重试次数的最大值之后,仍然没有正常接收到消息,则将该消息放入“死信”topic中去;

死信主题名称:%DLQ%+当前消费者组名称,例如:%DLQ%retry-consumer-group

方案1

设置重试次数,将超出重试次数限制的消息放入死信主题,然后再消费

代码如下·:

@Test
    public void retryConsumer() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-consumer-group");

        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);

        consumer.subscribe("retryTopic","*");

        //设置重试次数
        //重试的时间间隔如下:10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
        //最多可以重试的次数:并发模式下16次;顺序模式下为int的最大值,如果超过重试次数,则将消息放入死信主题中去

        consumer.setMaxReconsumeTimes(2);
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messageExts, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                MessageExt messageExt = messageExts.get(0);
                System.out.println("==========================");
                System.out.println(new Date());
                System.out.println("重试次数:" + messageExt.getReconsumeTimes());
                String msg = new String(messageExt.getBody());
                System.out.println("收到消息:" + msg);
                System.out.println("==========================");
                // 业务报错了 返回null 返回 RECONSUME_LATER 都会重试
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        });

        consumer.start();

        System.in.read();
    }

运行结果如下:

这是可以在面板中查看到该死信主题:

其中有一条消息,就是刚才没有被正常消费的那一条:

此时可以消费死信主题中的内容,然后进行处理(写入日志、mysql或报警处理等):

 //消费死信主题
    @Test
    public void retryDeadConsumer() throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-dead-consumer-group");
        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);
        consumer.subscribe("%DLQ%retry-consumer-group", "*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                MessageExt messageExt = msgs.get(0);
                System.out.println(new Date());
                System.out.println(new String(messageExt.getBody()));
                System.out.println("对死信主题中的内容进行处理");
                // 业务报错了 返回null 返回 RECONSUME_LATER 都会重试
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }

结果如下:

在面板中也可以看到,已经成功消费:


方案2

不设置重试次数,而是在代码中动态判定,如果超出了我们预期的重试次数,则停止重试,并进行相应处理,否则继续重试(这样就不用再去订阅死信主题进行消费了)

代码如下:

@Test
    public void retryConsumer2() throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-consumer-group");
        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);
        consumer.subscribe("retryTopic", "*");
        // 设定重试次数
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                MessageExt messageExt = msgs.get(0);
                System.out.println(new Date());
                // 业务处理
                try {
                    handleDb();
                } catch (Exception e) {
                    // 如果业务不正常,则重试
                    int reconsumeTimes = messageExt.getReconsumeTimes();
                    if (reconsumeTimes >= 3) {
                        // 不要重试了
                        System.out.println("记录到特别的位置 文件 mysql 通知人工处理");
                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                    }
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                // 如果业务正常,则消费成功
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }

    private void handleDb() {
        int i = 10 / 0;
    }

通过handleDb来模拟业务中出现异常的情况;

结果如下:

重试三次后就不再重试了

这时不会将消息放入死信topic中,而是直接进行处理,这里的处理机制和方案1中订阅死信topic之后的消费逻辑完全相同;

6.2 RocketMQ消息重复消费问题

问题出现的原因

  1. 生产者重复投递
  2. 消费者扩容(reBalance):一个消费者新上线后,同组的所有消费者要重新负载均衡(反之一个消费者掉线后,也一样)。一个队列所对应的新的消费者要获取之前消费的offset(偏移量,也就是消息消费的点位),此时之前的消费者可能已经消费了一条消息,但是并没有把offset提交给broker,那么新的消费者可能会重新消费一次。虽然orderly模式是前一个消费者先解锁,后一个消费者加锁再消费的模式,比起concurrently要严格了,但是加锁的线程和提交offset的线程不是同一个,所以还是会出现极端情况下的重复消费

解决方案

采用去重方案来解决:

生产者发送消息时携带一个唯一标识(key),与业务有关

消费者需要控制消息的幂等性

具体思路:在数据库(mysql,redis)中设计一张表,其中有一个字段与生产者发送消息时携带的key相对应,并给该字段添加唯一索引,这样在消息消费时就向表中插入一条记录,表示该条消息已经消费过了,这样如果重复消费的话,就会在插入数据时报错,这样我们就知道该条消息被重复消费了,因此不再继续往下处理;同理,在消费数据处理业务逻辑的时候,如果业务逻辑报错,需要将之前插入的记录删除掉,以便重试机制正常运行;

代码

首先创建好去重表,并给字段order_sn添加唯一索引:

具体代码如下: 

public class JRepeatTest {
    @Test
    public void repeatProducer() throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("repeat-producer-group");
        producer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);
        producer.start();
        String key = UUID.randomUUID().toString();
        System.out.println(key);
        // 测试 发两个key一样的消息
        Message m1 = new Message("repeatTopic", null, key, "扣减库存-1".getBytes());
        Message m1Repeat = new Message("repeatTopic", null, key, "扣减库存-1".getBytes());
        producer.send(m1);
        producer.send(m1Repeat);
        System.out.println("发送成功");
        producer.shutdown();
    }

    /**
     * 幂等性(mysql的唯一索引, redis(setnx) )
     * 多次操作产生的影响均和第一次操作产生的影响相同
     * 新增:普通的新增操作  是非幂等的,唯一索引的新增,是幂等的
     * 修改:看情况
     * 查询: 是幂等操作
     * 删除:是幂等操作
     * ---------------------
     * 我们设计一个去重表 对消息的唯一key添加唯一索引
     * 每次消费消息的时候 先插入数据库 如果成功则执行业务逻辑 [如果业务逻辑执行报错 则删除这个去重表记录]
     * 如果插入失败 则说明消息来过了,直接签收了
     *
     * @throws Exception
     */
    @Test
    public
    void repeatConsumer() throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("repeat-consumer-group");
        consumer.setNamesrvAddr(MqConstant.NAMESERVER_ADDR);
        consumer.subscribe("repeatTopic", "*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                // 获取key-唯一标识
                MessageExt messageExt = msgs.get(0);
                String keys = messageExt.getKeys();
                // 创建数据库连接
                Connection connection = null;
                try {
                    connection = DriverManager.getConnection("jdbc:mysql://hadoop102:3306/test?serverTimezone=GMT%2B8&useSSL=false", "root", "hadoop");
                } catch (SQLException e) {
                    e.printStackTrace();
                }
                PreparedStatement statement = null;

                try {
                    // 插入数据库 因为我们 key做了唯一索引
                    statement = connection.prepareStatement("insert into order_oper_log(`type`, `order_sn`, `user`) values (1,'" + keys + "','123')");
                } catch (SQLException e) {
                    e.printStackTrace();
                }

                try {
                    // 新增 要么成功 要么报错   修改 要么成功,要么返回0 要么报错
                    statement.executeUpdate();
                } catch (SQLException e) {

                    if (e instanceof SQLIntegrityConstraintViolationException) {
                        // 唯一索引冲突异常
                        System.out.println("repeat consume");
                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                    }
                    e.printStackTrace();
                }

                // 处理业务逻辑
                // 如果业务报错 则删除掉这个去重表记录 delete order_oper_log where order_sn = keys;
                System.out.println(new String(messageExt.getBody()));
                System.out.println(keys);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }
}

运行结果

可以看到数据库中成功记录了该key:

所以当消费重复消息时,在第二次插入数据时会出现异常,这样就避免了消息的重复消费

说明:本笔记为动力节点B站网课学习笔记

网课链接:动力节点RocketMQ全套视频教程-5小时学会rocketmq消息队列_哔哩哔哩_bilibili

配套资源:百度网盘 请输入提取码

(资源为网课配套资源,由官方提供,如链接失效可关注官方微信公众号获取)

  • 29
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

THE WHY

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

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

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

打赏作者

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

抵扣说明:

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

余额充值