消息队列MQ

MQ概述

1.MQ简介

MQ,Message Queue,是一种提供消息队列服务的中间件,也称为消息中间件,是一套提供了消息生产、储、消费全过程API的软件系统。消息即数据。一般消息的体量不会很大。

2.MQ用途

限流削峰

MQ可以将系统的超量请求暂存其中,以便系统后期可以慢慢进行处理,从而避免了请求的丢失或系统被压垮。

异步解耦

上游系统对下游系统的调用若为同步调用,则会大大降低系统的吞吐量与并发度,且系统耦合度
太高。而异步调用则会解决这些问题。所以两层之间若要实现由同步到异步的转化,一般性做法就是,在这两层间添加一个MQ层。

数据收集

分布式系统会产生海量级数据流,如:业务日志、监控数据、用户行为等。针对这些数据流进行实时或批量采集汇总,然后对这些数据流进行大数据分析,这是当前互联网平台的必备技术通过MQ完成比类数据收集是最好的选择。


3.常见的MQ产品

ActiveMQ

ActiveMQ是使用Java语言开发一款MQ产品。早期很多公司与项目中都在使用。但现在的社区活跃度已经很低。现在的项目中已经很少使用了。


RabbitMQ

RabbitMQ是使用ErLang语言开发的一款MQ产品。其吞吐量较Kafka与RocketMQ要低,且由于其不是Java语言开发,所以公司内部对其实现定制化开发难度较大。


Kafka

Kafka是使用Scala/Java语言开发的一款MQ产品。其最大的特点就是高吞吐率,常用于大数据领域的实时计算、日志采集等场景。其没有遵循任何常见的MQ协议,而是使用自研协议。对于Spring CloudNetcix,其仅支持RabbitMq与Kafka.


 


RocketMQ

RocketMQ是使用Java语言开发的一款MQ产品。经过数年阿里双 11 的考验,性能与稳定性非常高。其没有遵循任何常见的MQ协议,而是使用自研协议。对于Spring Cloud Alibaba,其支持RabbitMQ、Kafka,但提倡使用RocketMQ.



RocketMQ的安装与启动

下载与安装

1.下载RocketMQ

下载 | RocketMQ

2.windows下解压安装,配置环境变量ROCKETMQ_HOME

注意路径不要有中文或者空格等特殊字符


RocketMQ部署

1.启动nameserver

鼠标双击执行bin目录下的mqnamesrv.cmd 文件 或者 cmd 打开控制台,切换目录到rocket的bin目录下,执行命令:

start mqnamesrv.cmd

注:如果出现“找不到主类”的问题,需要将ROCKMQ_HOME和JAVA _HOME两个变量路径改掉,换成没有空格的路径


2.启动broker

执行bin目录下的 mqbroker.cmd 文件 或者 cmd 打开控制台,切换到bin目录下,执行命令:

start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true

默认端口是9876,autocreateTopicEnable=true表示允许自动创建topic

注:rocketMq运行环境中的内存不足会导致启动失败,可以修改配置文件
runserver.sh/runserver.cmd,runbroker.sh/runbroker.cmd中的配置参数


3.RocketMQ插件安装

访问 https://github.com/apache/rocketmq-externals/tags

下载rocketmq-console-1.0.0,并解压

在Eclipse中导入项目:

修改pom.xml文件中的java环境为当前环境1.8并执行compile指令

构建成功

在application.properties中设置地址namesrvAddr为localhost:9876

右键项目更新Maven

通过启动类App.java启动

在浏览器中打开


这里可以通过下面的指令将项目打包为jar包,失败可以尝试使用管理员身份运行

clean package -Dmaven.test.skip=true

打包成功后刷新项目可以在target文件夹下看到刚生成的jar包

此时复制jar包到到工作文件夹下通过cmd可以直接启动项目

java -jar rocketmq-console-ng-1.0.0.jar

同样可以在浏览器中打开


在bin目录下通过cmd输入指令mqadmin

mqadmin是一个命令行工具,用于管理Apache RocketMQ消息队列系统。它提供了一组命令,用于创建、删除、查询和管理消息队列和主题,以及监控和诊断RocketMQ集群的状态和性能。


基本概念

消息(Message)

消息是指,消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于个主题。

主题(Topic)

Topic表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。topic:message 1:n message:topic 1:1

一个生产者可以同时发送多种Topic的消息;而一个消费者只对某种特定的Topic感兴趣,只可以订阅和消费一种Topic的消息。producer:topic 1:n consumer:topic 1:1

标签(Tag)

为消息设置的标签,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。

Topic是消息的一级分类,Tag是消息的二级分类。

队列(Queue)

存储消息的物理实体。一个Topic中可以包含多个Queue,每个Queue中存放的就是该Topic的消息。一个Topic的Queue也被称为一个Topic中消息的分区(Partition)。

一个Topic的Queue中的消息只能被一个消费者组中的一个消费者消费。一个Queue中的消息不允
许同一个消费者组中的多个消费者同时消费

消息标识(MessageId/Key)

RocketMQ中每个消息拥有唯一的MessageId,且可以携带具有业务标识的Key,以方便对消息的查询。

不过需要注意的是,MessageId有两个:在生产者send()消息时会自动生成一个Messaged(msgId),当消息到达Broker后,Broker也会自动生成一个Messageld(offsetMsgId)。

msgId、offsetMsgId与key都称为消息标识。

  • msgId:由producer端生成,其生成规则为:producerIp+进程pid+MessageClientIDSetter类的ClassLoader的hashCode +当前时间+AutomicInteger自增计数器
  • offsetMsgId:由broker端生成,其生成规则为:brokerIp+物理分区的offset(Queue中的偏移量)
  • key:由用户指定的业务相关的唯一标识

1、普通消息

发送消息--生产者
同步发送

1.新建Maven项目并在pom.xml中导入依赖

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

2.创建测试类同步发送消息

public class EasyA {
    public static void main(String[] args) throws MQClientException,
         MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("easygroup");
 
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();
 
        //发送消息
        Message message = new Message("easytopic","rocketmq message".getBytes());
        SendResult result = producer.send(message);
        System.out.println(result);
        
        producer.shutdown();
    }
}

发送成功,在RocketMQ中,默认情况下一个Topic会创建4个队列。


异步发送
public class EasyB {
    public static void main(String[] args) throws MQClientException,
         RemotingException, InterruptedException {
        DefaultMQProducer producer=new DefaultMQProducer("easygroup");
 
        producer.setNamesrvAddr("127.0.0.1:9876");
 
        Message message=new Message("easytopic","Helloworld".getBytes());
 
        producer.start();
 
        producer.send(message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println(sendResult);
                System.out.println("OK");
            }
            @Override
            public void onException(Throwable e) {
                e.printStackTrace();
                System.out.println("Error");
            }
        });
 
        System.out.println("发送了一个消息");
        Thread.sleep(3000);
        producer.shutdown();
    }
}

单向发送

单向发送消息是指,Producer仅负责发送消息,不等待、不处理MQ的ACK。该发送方式时MQ不返回ACK。该方式的消息发送效率最高,但消息可靠性较差

public class EasyC {
 
    public static void main(String[] args) throws MQClientException,
             RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("easyGroup");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();
 
        Message message=new Message("easytopic","单向发送消息".getBytes());
        producer.sendOneway(message);
 
        producer.shutdown();
    }
}

发送成功


定义消息消费者 

public class EasyD {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer=new DefaultMQPushConsumer("easyGroup");
 
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
 
        //指定消费主题
        consumer.subscribe("easytopic","*");
        //设置监听器
        consumer.registerMessageListener(EasyD::consumeMessage);
 
        consumer.start();
        System.out.println("开始执行");
    }
 
    //处理方法
    static ConsumeConcurrentlyStatus consumeMessage(final List<MessageExt> msgs,
            final ConsumeConcurrentlyContext context){
        for (MessageExt m:msgs){
            System.out.println(new String(m.getBody()));
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
 
}


2.顺序消息

什么是顺序消息

顺序消息指的是,严格按照消息的发送顺序进行消费的消息(FIFO)。

默认情况下生产者会把消息以Round Robin轮询方式发送到不同的Queue分区队列;而消费消息时会从多个Queue上拉取消息,这种情况下的发送和消费是不能保证顺序的。如果将消息仅发送到同一个Queue中,消费时也只从这个Queue上拉取消息,就严格保证了消息的顺序性


为什么需要顺序消息

例如,现在有TOPIC ORDER STATUS(订单状态),其下有4个Queue队列,该Topic中的不同消息用于描述当前订单的不同状态。假设订单有状态:未支付、已支付、发货中、发货成功、发货失败。

根据以上订单状态,生产者从时序上可以生成如下几个消息:

订单T0000001:末支付->订单T0000001:已支付->订单T0000001:发货中->订单T0000001:发货失败

消息发送到MQ中之后,Queue的选择如果采用轮询策略,消息在MQ的存储可能如下:

这种情况下,我们希望Consumer消费消息的顺序和我们发送是一致的,然而上述MQ的投递和消费方式,我们无法保证顺序是正确的。对于顺序异常的消息,Consumer即使设置有一定的状态容错,也不能完全处理好这么多种随机出现组合情况。

基于上述的情况,可以设计如下方案:对于相同订单号的消息,通过一定的策略,将其放置在一个Queue中,然后消费者再采用一定的策略(例如,一个线程独立处理一个queue,保证处理消息的顺序性),能够保证消费的顺序性。


有序性分类

根据有序范围的不同,RocketMQ可以严格地保证两种消息的有序性:分区有序与全局有序。

全局有序

当发送和消费参与的Queue只有一个时所保证的有序是整个Topic中消息的顺序,称为全局有序。

在创建Topic时指定Queue的数量。有三种指定方式:

  1. 在代码中创建Producer时,可以指定其自动创建的Topic的Queue数量
  2. 在RocketMQ可视化控制台中手动创建Topic时指定Queue数量
  3. 使用mqadmin命令手动创建Topic时指定Queue数量


分区有序

如果有多个Queue参与,其仅可保证在该Queue分区队列上的消息顺序,则称为分区有序。

把需要有序的消息指定一个队列进行发送

一般性的选择算法是,让选择key(或其hash值)与该Topic所包含的Queue的数量取模,其结果即为选择出的Queue的QueueId。


代码举例

在前面同步发送消息时指定队列:

        //发送消息
        Message message = new Message("easytopic","rocketmq message".getBytes());
 
        MessageQueue queue=new MessageQueue("easytopic","LAPTOP-93F8NUKL",1);
        SendResult result = producer.send(message,queue);
        System.out.println(result);
        result = producer.send(message,queue);
        System.out.println(result);

发送成功:


3.延时消息

什么是延时消息

当消息写入到Broker后,在指定的时长后才可被消费处理的消息,称为延时消息。

采用RocketMQ的延时消息可以实现定时任务的功能,而无需使用定时器。典型的应用场景是,电商交易中超时未支付关闭订单的场景,12306平台订票超时未支付取消订票的场景。


延时等级

延时消息的延迟时长不支持随意时长的延迟,是通过特定的延迟等级来指定的。延时等级定义在
RocketMQ服务端的MessageStoreConfig类中的如下变量中:

若指定的延时等级为3,则表示延迟时长为10s,即延迟等级是从1开始计数的。

level==0,消息为非延迟消息

1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s

level>maxLevel,则level== maxLevel,例如level==20,延迟2h


延时消息实现原理

具体实现方案

        Producer将消息发送到Broker后,Broker会首先将消息写入到commitlog文件,然后需要将其分发到相应的consumequeue。不过,在分发前,系统会先判断消息中是否带有延时等级。若没有,则直接正常分发;若有则需要经历一个复杂的过程:

        修改消息的Topic为SCHEDULE TOPIC XXXX

        根据延时等级,在consumequeue目录中SCHEDULE TOPIC XXXX主题下创建出相应的queueld目录与consumequkue文件(如果没有这些目录与文件的话)。

        修改消息索引单元内容。索引单元中的Message Tag HashCode部分原本存放的是消息的Tag的Hash值。现修改为消息的投递时间。投递时间是指该消息被重新修改为原Topic后再次被写入到commitlog中的时间。投递时间=消息存储时间+延时等级时间。消息存储时间指的是消息被发送到Broker时的时间戳。

        将消息索引写入到SCHEDULE TOPIC XXXX主题下相应的consumequeue中

        投递延时消息

        将消息重新写入commitlog

代码实例

在发送消息时进行如下设置:

        //发送消息
        Message message = new Message("easytopic","rocketmq message".getBytes());
        message.setDelayTimeLevel(5);

### Linux 中 `mq_open` 函数的使用说明 #### 函数原型 `mq_open` 是用于打开或创建一个消息队列的系统调用。其函数原型如下: ```c #include <mqueue.h> mqd_t mq_open(const char *name, int oflag); mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr); ``` --- #### 参数详解 - **`const char *name`**: 表示要操作的消息队列名称。该名称以 `/` 开头,例如 `/my_queue`。这是 POSIX 消息队列的标准命名方式。 - **`int oflag`**: 定义如何打开或创建消息队列。常见的标志位包括: - `O_RDONLY`: 只读模式。 - `O_WRONLY`: 只写模式。 - `O_RDWR`: 读写模式。 - `O_CREAT`: 如果消息队列不存在,则创建它。 - `O_EXCL`: 当与 `O_CREAT` 结合使用时,如果消息队列已经存在则报错。 - **`mode_t mode` (仅当设置了 `O_CREAT`)**: 设置新创建消息队列的权限,类似于文件权限掩码。通常使用八进制表示,例如 `S_IRUSR | S_IWUSR` 表示用户可读可写[^1]。 - **`struct mq_attr *attr` (仅当设置了 `O_CREAT`)**: 指向一个结构体指针,定义消息队列属性。如果不设置特定属性,可以传入 `NULL`。以下是 `mq_attr` 的成员: - `mq_flags`: 队列的操作标志(目前未被实现)。 - `mq_maxmsg`: 队列中允许的最大消息数。 - `mq_msgsize`: 单条消息最大字节数。 - `mq_curmsgs`: 当前队列中的消息数量(只读字段)。 --- #### 返回值 - 成功时返回一个有效的消息队列描述符 (`mqd_t`)。 - 失败时返回 `(mqd_t)-1` 并设置相应的错误码。可以通过 `strerror(errno)` 获取具体的错误信息。 --- #### 示例代码 以下是一个简单的例子,展示如何使用 `mq_open` 创建并打开一个消息队列: ```c #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/stat.h> #include <mqueue.h> #include <errno.h> #define QUEUE_NAME "/example_queue" #define MAX_MESSAGES 10 #define MESSAGE_SIZE 256 int main() { struct mq_attr attr; mqd_t mq; // 初始化消息队列属性 attr.mq_flags = 0; // 默认标志 attr.mq_maxmsg = MAX_MESSAGES; // 最大消息数 attr.mq_msgsize = MESSAGE_SIZE; // 每条消息大小 attr.mq_curmsgs = 0; // 当前消息数(初始化为0) // 打开/创建消息队列 mq = mq_open(QUEUE_NAME, O_CREAT | O_RDWR, 0644, &attr); if (mq == (mqd_t)-1) { // 判断是否失败 perror("mq_open failed"); printf("Error code: %s\n", strerror(errno)); exit(EXIT_FAILURE); } printf("Message queue opened successfully with descriptor: %d\n", mq); // 关闭消息队列 if (mq_close(mq) != 0) { perror("mq_close failed"); exit(EXIT_FAILURE); } // 删除消息队列 if (mq_unlink(QUEUE_NAME) != 0) { perror("mq_unlink failed"); exit(EXIT_FAILURE); } return EXIT_SUCCESS; } ``` --- #### 常见问题及解决方案 1. **无法找到消息队列**:确保传递给 `mq_open` 的名字是以 `/` 开头的有效路径名。 2. **权限不足**:检查是否有足够的权限访问或创建目标消息队列。 3. **资源耗尽**:如果系统达到最大消息队列限制,可能会导致 `mq_open` 调用失败。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值