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
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的数量。有三种指定方式:
- 在代码中创建Producer时,可以指定其自动创建的Topic的Queue数量
- 在RocketMQ可视化控制台中手动创建Topic时指定Queue数量
- 使用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);