目录
三、topic 、broker、messageQueue之间的关系
6.3.5 同步发送顺序消息(syncSendOrderly)
6.3.6 异步发送顺序消息(asyncSendOrderly)
我发现目前网上很多关于RocketMQ的教程都是零零散散的,不够系统。这篇文章花费了大量心血,查阅了很多资料,浏览了很多视频才总结下来的。
真心希望可以帮助到各位,如果有不足之处也请不吝指出,我们共同进步!
一、下载、安装
这部分请看:Linux环境下安装RocketMQ(单机、集群)_何苏三月的博客-CSDN博客
二、基本演示
2.1 创建项目导入依赖
创建一个maven项目,引入rocketmq的依赖。
依赖的版本和我们服务器的版本保持一致。
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.1</version>
</dependency>
2.2 生产者发送消息
package com.hssy.myrocketmqdemo.producer;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import java.nio.charset.StandardCharsets;
public class TestProducer {
public static void main(String[] args) throws Exception {
// 1. 创建一个生产者
DefaultMQProducer producer = new DefaultMQProducer("my-producer-group1");
// 2. 指定nameserver的地址,随便上哪个nameserver都可以。
producer.setNamesrvAddr("192.168.241.100:9876");
// 3. 启动生产者
producer.start();
// 4. 创建消息
for (int i = 0; i < 10; i++) {
Message message = new Message("TopicTest", "TagA", ("Hello,rocketmqqqq" + i).getBytes(StandardCharsets.UTF_8));
// 5. 发送消息
SendResult sendResult = producer.send(message);
System.out.println(sendResult);
}
// 6. 关闭连接
producer.shutdown();
}
}
2.3 消费者消费消息
package com.hssy.myrocketmqdemo.producer;
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.common.message.MessageExt;
import java.util.List;
public class TestConsumer {
public static void main(String[] args) throws MQClientException {
// 1. 创建一个消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my-consumer-group1");//指定一个消费者组,如果没有这个组会自动创建
// 2. 指明nameserver的地址
consumer.setNamesrvAddr("192.168.241.100:9876");
// 3. 订阅主题
consumer.subscribe("TopicTest","TagA");//订阅具体某一个主题的消息,subExpression可以指定具体主题下哪个tag,也可以设置为*,表示所有tags都可以消费到。
// 4. 创建一个监听器,当broker把消息推过来时调用
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for(MessageExt item : list){
System.out.println("收到消息:" + item);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; // 处理完成后告知broker成功收到消息。
}
});
// 5. 启动消费者
consumer.start();//启动后,只要有消息过来就会去调用上面的监听器执行里面的方法。
System.out.println("消费者已启动");
}
}
其中,body字段为消息体
我们可以通过String字符串来转换这个消息体
重新启动消费者,然后再启动生产者,让他再发十条消息给消费者消费。
三、topic 、broker、messageQueue之间的关系
例如,我们通过Java代码发送20条消息,从如下的结果中可以看出是符合上面关系说明的。
当然了,我们发送消息的时候也可以选择发送到具体哪个消息队列中。参数不填默认就是轮询发送到所有broker的4个队列。
通过上面的案例,我们简单知道了怎么发送消息,怎么消费消息,以及topic、broker、messagequeue之前的关系。
下面,我们依然是学习发送消息。
掌握简单消息的类型,比如什么是同步消息,异步消息以及单向消息。
四、普通消息
RocketMQ中,消息的类型分为:普通消息、顺序消息、事务消息等。这一章,我们先讲最基础的普通消息。
生产者发送的消息,普通消息的发送又分成三种:可靠同步发送、可靠异步发送、单向发送。
普通消息一般应用于微服务解耦、事件驱动、数据集成等场景,这些场景大多数要求数据传输通道具有可靠传输的能力,且对消息的处理时机、处理顺序没有特别要求。
4.1 普通消息生命周期
-
初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。
-
待消费:消息被发送到服务端,对消费者可见,等待消费者消费的状态。
-
消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,RocketMQ会对消息进行重试处理。所谓消息重试,是指:消费者出现异常,消费某条消息失败时, RocketMQ 会根据消费重试策略重新投递该消息进行故障恢复。
-
消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。 RocketMQ默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。
-
消息删除:Apache RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。更多信息,请参见消息存储和清理机制。
4.2 可靠同步发送
生产者发送消息后,必须等待broker返回信息后才继续之后的业务逻辑,在broker返回信息之前,生产者阻塞等待。
如果发送失败了,RocketMQ消息在发送时不会丢失,保证即使发送失败也会进行重试。如果某条消息发送过程中发生了错误,则rocketmq会尝试重新发送消息。 在这种情况下,如果重试成功,则可以确保消息可以被消费者准确地接收,否则将通过一些策略(例如暂停发送)来避免数据包丢失。因此,在实现可靠同步发送时,建议使用适当的参数进行配置,从而确保消息能够成功发送和传递。
我们前面的示例就是生产者发送的同步消息。
package com.hssy.myrocketmqdemo.simple;
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 java.nio.charset.StandardCharsets;
public class SyncProducer {
public static void main(String[] args) throws Exception {
// 1. 创建一个生产者,需要传一个生产者组参数
DefaultMQProducer producer = new DefaultMQProducer("producerGroup1");
// 2. 设置nameserver的地址
producer.setNamesrvAddr("192.168.241.102:9876");
// 3. 开启一个生产者
producer.start();
// 4. 创建消息
for (int i = 0; i < 100; i++) {
// 参数分别为:topic的名称,tag标签(只是用来过滤消息的),消息内容
Message message = new Message(
"my-topic-marry",
"tag-xiu",
("this is a message,no: " + i).getBytes(StandardCharsets.UTF_8)
);
// 5. 发送消息
SendResult sendResult = producer.send(message);
System.out.println("消息发送结果: " + sendResult);
}
// 6. 关闭生产者
producer.shutdown();
}
}
同步消息应用场景:如重要通知消息、短信通知、短信营销系统等。保证消息能够顺利抵达。
4.3 可靠异步发送
生产者发完消息后,不需要等待broker的回信,可以接着发送下一个数据包的通讯方式。也即不会阻塞后续代码的执行。只要调用了send()方法,就可以往下继续执行其他代码了。
这个过程的逻辑是:
当进行异步消息发送时,生产者会向RocketMQ Broker发送一条回查消息,询问该消息是否已经提交(表示已经成功)或未提交(表示还没成功)。Broker接收到回查消息后,会根据消息ID来查询消息事务状态,并返回给生产者。生产者通过回调函数获取到这个状态信息,根据状态信息来决定是否需要重试失败的消息。
但是由于是异步发送:
在异步发送消息的过程中,生产者会同时执行其他逻辑。如果消息发送失败,由于RocketMQ是异步发送消息,其他业务逻辑可能已经执行完毕了。假如这部分执行完毕的业务逻辑必须要确保消息发送成功才有效,那么此时就不可靠了。
所以,个人认为,异步发送的可靠性,在一定程度上也取决于开发者自身对可靠性的加固。比如,一旦确认消息发送失败,则进行回滚等操作来保证业务的可靠性。
当然,前面也提到了,回调函数获取到这个状态信息后,我们可以在回调函数相应的状态方法里面去处理,例如是否需要重试发送。
异步发送,说白了,这种方式如果用的适当,可以提高系统响应速度,提高用户体验。
但是它也可能存在消息丢失或者重复发送等异常的问题。
以下是代码演示
package com.hssy.myrocketmqdemo.simple;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
public class AsyncProducer {
public static void main(String[] args) throws Exception {
// 1. 创建生产者
DefaultMQProducer producer = new DefaultMQProducer("producerGroup1");
// 2. 设置nameserver地址
producer.setNamesrvAddr("192.168.241.101:9876");
// 3. 启动生产者
producer.start();
// 选配:设置消息异步发送失败后重试次数
producer.setRetryTimesWhenSendAsyncFailed(0);
// 选配:设置闭锁,目的是打印出失败的情况,方便观看
int messageCount = 100;
CountDownLatch countDownLatch = new CountDownLatch(messageCount);
for (int i = 0; i < messageCount; i++) {
int index = i;
// 4. 创建消息
Message message = new Message(
"my-topic-lorry",
"tag-mei",
("lorry,come" + i).getBytes(StandardCharsets.UTF_8)
);
// 5. 发送消息,需要额外传递一个回调函数,作用是供broker调用,这样就可以异步发送消息了
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 选配部分:发送成功,闭锁-1
countDownLatch.countDown();
System.out.println("发送消息结果: " + "当前是第 " + index + "条消息, " + sendResult);
}
@Override
public void onException(Throwable e) {
// 选配部分:发送失败,闭锁也要-1
countDownLatch.countDown();
System.out.println("发送消息异常: " + "当前是第 " + index + "条消息, " + e);
}
});
}
// 由于是异步发送消息,所以不会阻碍后续代码。
// 只要调用了发送方法,无需等待broker调用上面的回调函数,可以继续往下执行
System.out.println("-----------------------------------");
// 选配部分: 闭锁阻塞,防止主程序结束,直到闭锁消耗完毕自动结束
countDownLatch.await();
// 6. 关闭生产者
producer.shutdown();
}
}
异步发送,适用于响应时间敏感的业务场景。可能存在消息丢失或者重复发送等异常的问题,对于一致性要求不那么高的场景可以使用。比如推送日志等等。
4.4 单向发送
生产者发送完消息后不需要等待任何回复,直接进行之后的业务逻辑,单向传输用于需要中等可靠性的情况,例如日志收集。
它和异步消息都是不需要回复,继续执行后续代码的。不同之处在于异步消息还需要发送回调函数给服务器broker,而单向消息则不需要。
换句话说:单向发送是指发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。如果对数据的可靠性要求不高,丢失了也没关系,如日志收集这种场景,可以采用这种方式发送消息,这种消息发送方式是速度最快的。
package com.hssy.myrocketmqdemo.simple;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import java.nio.charset.StandardCharsets;
public class OnewayProducer {
public static void main(String[] args) throws Exception {
// 1. 创建一个生产者
DefaultMQProducer producer = new DefaultMQProducer("producerGroup1");
// 2. 设置连接的nameserver地址
producer.setNamesrvAddr("192.168.241.100:9876;192.168.241.102:9876"); // 为了避免某台nameserver挂掉连不上,建议多写几台
// 3. 启动生产者
producer.start();
for (int i = 0; i < 100; i++) {
// 4. 创建消息
Message message = new Message(
"oneway-topic-demo",
"April",
("本台消息,消息编号为:" + i).getBytes(StandardCharsets.UTF_8)
);
// 5. 发送单向消息
producer.sendOneway(message);
}
System.out.println("----------------------");
Thread.sleep(5000); // 保证消息发完再关闭程序
// 6. 关闭生产者
producer.shutdown();
}
}
五、顺序消息
顺序消息是消息队列提供的一种严格按照顺序来发布和消费的消息类型。
在发送消息时,可以为每个消息设置一个关键字,RocketMQ通过这个关键字来决定消息存储在哪个队列中。如果多条消息使用相同的关键字,则这些消息会被存储到同一个队列中。消费者从这个队列中依次消费消息,保证了消息的顺序性。
或者通过合理的Topic和Queue设计也可以实现顺序消息。比如,可以将订单消息放到一个专门的OrderTopic中,根据订单ID生成多个队列,将同一个订单的消息都路由到同一个队列中进行消费,从而保证了订单的有序性。
需要注意的是,RocketMQ的顺序消息只保证消息在同一个队列中的顺序,不保证跨队列的顺序。因此,在设计顺序消息业务时,需要根据具体业务需求灵活选择以上两种方式。
5.1 如何保证消息的顺序性?
RocketMQ 的消息的顺序性分为两部分,生产顺序性和消费顺序性。
5.1.1 生产顺序性
Apache RocketMQ 通过生产者和服务端的协议保障单个生产者串行地发送消息,并按序存储和持久化。
如需保证消息生产的顺序性,则必须满足以下条件:
-
单一生产者:消息生产的顺序性仅支持单一生产者,如果不同生产者分布在不同的系统,即使设置相同的消息组,不同生产者之间产生的消息也无法判定其先后顺序。
-
串行发送:Apache RocketMQ 生产者客户端支持多线程安全访问,但如果生产者使用多线程并行发送,则不同线程间产生的消息将无法判定其先后顺序。
满足以上条件的生产者,将顺序消息发送至 Apache RocketMQ 后,会保证设置了同一消息组的消息,按照发送顺序存储在同一队列中。服务端顺序存储逻辑如下:
-
相同消息组的消息按照先后顺序被存储在同一个队列。
-
不同消息组的消息可以混合在同一个队列中,且不保证连续。
如上图所示,消息组1和消息组4的消息混合存储在队列1中, Apache RocketMQ 保证消息组1中的消息G1-M1、G1-M2、G1-M3是按发送顺序存储,且消息组4的消息G4-M1、G4-M2也是按顺序存储,但消息组1和消息组4中的消息不涉及顺序关系。
5.1.2 消费顺序性
Apache RocketMQ 通过消费者和服务端的协议保障消息消费严格按照存储的先后顺序来处理。
如需保证消息消费的顺序性,则必须满足以下条件:
-
投递顺序
Apache RocketMQ 通过客户端SDK和服务端通信协议保障消息按照服务端存储顺序投递,但业务方消费消息时需要严格按照接收---处理---应答的语义处理消息,避免因异步处理导致消息乱序。
备注:
消费者类型为PushConsumer时, Apache RocketMQ 保证消息按照存储顺序一条一条投递给消费者,若消费者类型为SimpleConsumer,则消费者有可能一次拉取多条消息。此时,消息消费的顺序性需要由业务方自行保证。消费者类型的具体信息,请参见消费者分类。
-
有限重试
Apache RocketMQ 顺序消息投递仅在重试次数限定范围内,即一条消息如果一直重试失败,超过最大重试次数后将不再重试,跳过这条消息消费,不会一直阻塞后续消息处理。
对于需要严格保证消费顺序的场景,请务设置合理的重试次数,避免参数不合理导致消息乱序。
5.1.3 生产顺序性和消费顺序性组合
如果消息需要严格按照先进先出(FIFO)的原则处理,即先发送的先消费、后发送的后消费,则必须要同时满足生产顺序性和消费顺序性。
一般业务场景下,同一个生产者可能对接多个下游消费者,不一定所有的消费者业务都需要顺序消费,您可以将生产顺序性和消费顺序性进行差异化组合,应用于不同的业务场景。例如发送顺序消息,但使用非顺序的并发消费方式来提高吞吐能力。更多组合方式如下表所示:
生产顺序 | 消费顺序 | 顺序性效果 |
---|---|---|
设置消息组,保证消息顺序发送。 | 顺序消费 | 按照消息组粒度,严格保证消息顺序。 同一消息组内的消息的消费顺序和发送顺序完全一致。 |
设置消息组,保证消息顺序发送。 | 并发消费 | 并发消费,尽可能按时间顺序处理。 |
未设置消息组,消息乱序发送。 | 顺序消费 | 按队列存储粒度,严格顺序。 基于 Apache RocketMQ 本身队列的属性,消费顺序和队列存储的顺序一致,但不保证和发送顺序一致。 |
未设置消息组,消息乱序发送。 | 并发消费 | 并发消费,尽可能按照时间顺序处理。 |
5.2 局部顺序消费
局部消费指的是消费者消费某个topic的某个队列中的消息是顺序的。消费者使用MessageListenerOrderly类做消息监听,实现局部顺序消费。
我们先来创建一个生产者,要求让它按照我们指定的队列规则给队列发送消息。
package com.hssy.myrocketmqdemo.demo;
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.MessageQueue;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* 指定消息队列生产者
*/
public class AssignQueueProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producerGroup1");
producer.setNamesrvAddr("192.168.241.100:9876");
producer.start();
for (int i = 0; i < 10; i++) {
int orderId = i;
for (int j = 0; j < 5; j++) {
Message message = new Message(
"sequence-topic-demo-1",
"subTag",
("这是一条消息,消息的编号为: " + i).getBytes(StandardCharsets.UTF_8)
);
// 发送消息,指定消息队列
// 如果不指定消息队列默认就是轮询,依次发送消息给每个broker,每个消息队列。
// 这里我们可以通过消息队列选择器,自定义要发送的队列中
SendResult sendResult = producer.send(
message,
new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 可以指定哪一个队列,(默认 2 主 2 从,自动创建主题的话,就是8个队列)
// 比如我们指定具体某一固定队列
// return mqs.get(1);
// 或者按照某种规则设置队列
// 这里的Object arg参数就是这里send方法的第三个参数(通过源码发现的)
// 比如我们可以根据前面定义的orderId来取模获得队列
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
},
orderId
);
System.out.println("发送消息结果: " + sendResult);
}
}
producer.shutdown();
}
}
可以看到是按照队列编号,顺序发送的。先给队列0顺序发送5条消息,然后顺序切换到队列2有顺序发送5条消息,以此类推。
接下来创建消费者,让它实现局部消费。
package com.hssy.myrocketmqdemo.consumer;
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.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class SequenceConsumer {
public static void main(String[] args) throws Exception {
// 1. 创建一个消费者,并给它指定一个消费者组
// push模式是broker主动推送消息给消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("subSequence-consume-group-1");
// 2. 设置nameserver地址
consumer.setNamesrvAddr("192.168.242.100:9876;192.168.241.102:9876");
// 选配:设置从哪里消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);// 从第一个offset处开始消费
// 3. 订阅主题,设置过滤标签
consumer.subscribe("sequence-topic-demo-2","*");
// 4. 设置监听器,注意传递的参数是带order顺序的那个接口
consumer.setMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
for(MessageExt item : msgs){
System.out.println("收到消息,消息内容为 " + new String(item.getBody()) + " 全部信息:" + item);
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
// 5. 启动消费者
consumer.start();// 启动之后,它就会去执行上面设置的监听器方法。
System.out.println("消费者已启动");
}
}
貌似一个消费者我们看不出什么,启动多个消费者试试。这里就不继续演示了,感兴趣的可以自行尝试。
六、Springboot整合RocketMQ
6.1 导入依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
6.2 application.properties
# 必填:通过名称空间地址连接集群
rocketmq.name-server=192.168.241.100:9876;192.168.241.101:9876
# 必填:指定生产者group,否则无法发送。生产者一定要有生产者组,即使只有一个生产者
rocketmq.producer.group=smart-aide-prod-group-1
# 同步发送消息失败重试次数,默认2
rocketmq.producer.retry-times-when-send-failed=3
# 异步发送消息失败重试次数,默认2
rocketmq.producer.retry-times-when-send-async-failed=3
6.3 生产者发送消息
6.3.1 发送同步消息(convertAndSend)
@ApiOperation(value = "测试发送convertAndSend(同步发送)")
@PostMapping("convertAndSendTest")
public Result<String> convertAndSendTest(@RequestBody User user){
// RocketMQTemplate有 send 方法和 convertAndSend 方法
// 都可以用来发送消息
// 区别:前者的方法入参是rocketmq规定的Message类型,而后者可以发送对象,并且帮我们转换
// 演示下:
// 两个参数:D destination, Object payload
// 第一个参数是目的地,也就是消息要发送到的topic
// 第二个参数就是消息本身
// 由于我们在搭建集群时,指定了autoCreateTopicEnable=true
// 所以即使该topic不存在,rocket也会帮我们创建好
// convertAndSend方法底层其实是syncSend方法,都是以同步的方法实现消息的发送
// 但使用syncSend() 方法,因为没有对参数进行了序列化,运行速度会慢很多
// 关于syncSend()方法的一些其他特点,后面也会讲解
rocketMQTemplate.convertAndSend("sa-userInfo-topic",user);
return Result.success();
}
@ApiOperation("测试批量发送convertAndSend(同步发送)")
@PostMapping("/convertAndSendBatch")
public Result<String> convertAndSendBatch(int count) throws Exception {
if (count<0){
throw new Exception("批量发送的条数不能小于0");
}
for (int i = 0; i < count; i++) {
// 发送的消息默认是按照broker和队列轮询的方式
rocketMQTemplate.convertAndSend("sa-test1-topic","批量发送测试消息:" + UUID.randomUUID().toString().substring(0,9));
}
log.info("批量消息发送完成,共计{}条",count);
return Result.success();
}
6.3.2 发送同步消息(syncSend)
@ApiOperation("测试批量发送syncSend(同步发送)")
@PostMapping("/syncSendBatch")
public Result<String> syncSendBatch(int count) throws Exception {
if (count<0){
throw new Exception("批量发送的条数不能小于0");
}
for (int i = 0; i < count; i++) {
// syncSend方法发送的消息会按照某种算法分散到集群是各台broker上
// 而且默认采用轮询的方式,按照broker和消息队列逐一发送,
// 这和convertAndSend方法的结果其实是一样的
SendResult sendResult = rocketMQTemplate.syncSend("sa-syncSend-test-topic", "批量同步发送测试消息,随机值:" + UUID.randomUUID().toString());
log.info("发送消息成功!消息id:{},对象消息id:{},消息队列:{},队列点位:{},发送状态{}" ,
sendResult.getMsgId(),
sendResult.getOffsetMsgId(),
sendResult.getMessageQueue(),
sendResult.getQueueOffset(),
sendResult.getSendStatus()
);
}
System.out.println("同步消息批量发送完成,共计 " + count + " 条");
return Result.success();
}
6.3.3 发送异步消息(asyncSend)
@ApiOperation(value = "测试批量发送asyncSend(异步发送)")
@PostMapping("asyncSendBatch")
public Result<String> asyncSendBatch(int count) {
// 异步发送会按照某种算法分配到集群中所有的broker上
// 每个broker上的具体队列也是按照顺序轮询的
// 但是由于是异步发送,一旦某个队列的还没有处理完,但又轮询到它时
// 就会跳过让其他队列接收处理。
// 所以批量发送的结果有可能就是不同队列接收的消息条数可能有所差异。
// 然后就是在异步执行的时候,即使方法执行完毕,也不会影响异步任务的继续执行。
// 它会等到broker确认后,通过该回调参数看到结果。
for (int i = 0; i < count; i++) {
rocketMQTemplate.asyncSend("sa-asyncSend-test-topic", "此为异步发送消息,随机值:"+UUID.randomUUID().toString(), new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("发送消息成功!消息id:{},对象消息id:{},消息队列:{},队列点位:{},发送状态{}" ,
sendResult.getMsgId(),
sendResult.getOffsetMsgId(),
sendResult.getMessageQueue(),
sendResult.getQueueOffset(),
sendResult.getSendStatus()
);
}
@Override
public void onException(Throwable throwable) {
log.error("消息发送异常,错误信息:{}",throwable.getMessage());
}
});
}
System.out.println("异步执行...");
return Result.success();
}
6.3.4 发送单向消息(sendOneWay)
@ApiOperation(value = "测试批量发送sendOneWay(单向发送)")
@PostMapping("sendOneWay")
public Result<String> sendOneWay(int count) {
// 单向发送也是异步的,只是它不用等待broker确认
// 也即发没发送成功它是不管的
for (int i = 0; i < count; i++) {
rocketMQTemplate.sendOneWay("sa-oneway-test-topic","此为单向发送消息,消息随机值:" + UUID.randomUUID().toString());
}
System.out.println("单向发送,也是异步执行...");
return Result.success();
}
6.3.5 同步发送顺序消息(syncSendOrderly)
@ApiOperation(value = "测试批量发送syncSendOrderly(顺序消息同步发送)")
@PostMapping("syncSendOrderlyBatch")
public Result<String> syncSendOrderlyBatch(int count) {
for (int i = 0; i < count; i++) {
// hashKey 用来计算决定消息发送到哪个消息队列, 一般是订单ID,产品ID等
// 只要保证hashKey相同,就会发送到同一个broker的同一个队列中
SendResult sendResult = rocketMQTemplate.syncSendOrderly(
"order-message-test-topic",
"此为顺序消息,随机值:" + UUID.randomUUID().toString(),
"i520love1314you"
);
log.info("发送消息成功!消息id:{},对象消息id:{},消息队列:{},队列点位:{},发送状态{}" ,
sendResult.getMsgId(),
sendResult.getOffsetMsgId(),
sendResult.getMessageQueue(),
sendResult.getQueueOffset(),
sendResult.getSendStatus()
);
}
System.out.println("顺序消息批量同步发送完成,共计 " + count + " 条");
return Result.success();
}
6.3.6 异步发送顺序消息(asyncSendOrderly)
@ApiOperation(value = "测试批量发送asyncSendOrderly(顺序消息异步发送)")
@PostMapping("asyncSendOrderlyBatch")
public Result<String> asyncSendOrderlyBatch(int count) {
for (int i = 0; i < count; i++) {
// hashKey 用来计算决定消息发送到哪个消息队列, 一般是订单ID,产品ID等
// 只要保证hashKey相同,就会发送到同一个broker的同一个队列中
// 只不过异步发送顺序消息的话可能会存在问题,和前面异步发送一样,
// 如果当前点位还没有确认处理的话,那么就会落入下一个点位。导致顺序乱序。
// 不信的话启动消费者进行顺序消费的时候我们可以看到
// 所以顺序消息,最好的选择就是同步发送顺序消息,而不要异步发送。
rocketMQTemplate.asyncSendOrderly(
"order-message-test-topic",
"此为顺序消息,顺序号:" + count,
"123123",
new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("发送消息成功!消息id:{},对象消息id:{},消息队列:{},队列点位:{},发送状态{}" ,
sendResult.getMsgId(),
sendResult.getOffsetMsgId(),
sendResult.getMessageQueue(),
sendResult.getQueueOffset(),
sendResult.getSendStatus()
);
}
@Override
public void onException(Throwable throwable) {
log.error("消息发送失败!信息为:{}",throwable.getMessage());
}
}
);
}
System.out.println("顺序消息批量异步发送完成,共计 " + count + " 条");
return Result.success();
}
6.3.7 发送定时/延时消息
上述的发送同步、异步、单向、顺序消息都有相应的延时时间参数的重载方法,只需设置相应的事时间参数即为对应的定时/延时消息。这里就不演示了。
6.3.8 关于过滤标签、索引Key的小提示
过滤标签Tag(可选)
-
定义:消息的过滤标签。消费者可通过Tag对消息进行过滤,仅接收指定标签的消息。
-
取值:由生产者客户端定义。
-
约束:一条消息仅支持设置一个标签。
用法:我们只需要在各自发送消息方法的主题后面拼接“:标签名”即可。这在源码中会自动做处理:
索引Key列表(可选)
-
定义:消息的索引键,可通过设置不同的Key区分消息和快速查找消息。
-
取值:由生产者客户端定义。
6.4 消费者消费消息
6.4.1 样例演示
消费者消费消息,在整合springboot中,通常是通过创建实现类来完成,该类需要交给spring进行管理,这样项目启动后,通过实现类的onMessage方法就会去监听消息,一旦监听到消息,就会自动进行接收。
代码演示:
package com.hssy.smartaide.mq;
import lombok.extern.slf4j.Slf4j;
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;
/**
* topic 需要和生产者的topic一致,这样才能消费到想要的消息
* consumerGroup 消费者必须关联到指定的消费者分组,通过消费者分组获取消费行为。
* 这个可以任意选择,如果有多个消费者时,需要考虑他们之间的关系。
* selectorExpression的意思指的就是tag,默认为“*”,不设置的话会监听所有消息
* consumeMode 消息的投递模式,默认是并发投递
* 我们可以选择ORDERLY顺序投递到消费者组,前提是发送的消息也必须是顺序才有效
* ORDERLY默认是单线程的。所以同一个消费者组,即使存在多个消费者,也不会发生消息失序。
* 说明:
* 如果使用ConsumeMode.ORDERLY消费模式,
* 则同一消费组内的多个消费者将会按照消息顺序进行负载均衡,
* 也就是说每个消息只会分配给一个消费者进行处理,减轻一个消费者的压力。
* 但是主题中仅有一个队列时,由于有一个队列中的消息只能被一个消费者消费,
* 所以增加消费者并不会影响消费的顺序。
*
* 举例来说,
* 假设当前主题只有一个队列,并且有3个消费者C1、C2、C3在同一消费者组里,
* 并且ConsumeMode设置为ORDERLY。
* 当有一条消息M进入主题时,RocketMQ 会选择其中一个消费者进行消费,比如C1。
* 之后,所有的消息都会由C1消费,直到C1关闭或宕机。
* 如果在此期间,C2或C3尝试消费任何消息,那么该消息会留待C1的处理完成之后再分配给它们,
* 这确保了同个队列的消息总会按照顺序被提交给消费者。
* messageModel 消费的模式,有两种,
* 默认是集群模式或者叫负载均衡 MessageModel.CLUSTERING
* 另外一种不太常用:广播模式 MessageModel.BROADCASTING
*/
@Slf4j
@Component
@RocketMQMessageListener(
topic = "order-message-test-topic",
consumerGroup = "demo-consumer-group",
selectorExpression = "*",
consumeMode = ConsumeMode.ORDERLY)
public class DemoConsumer implements RocketMQListener<MessageExt> {
// 泛型可以用String或者用实体类接收
// 一旦监听到消息就会执行onMessage方法。
//@Override
//public void onMessage(String s) {
// log.info("收到消息:{}",s);
//}
// 也可以使用MessageExt接收,该类的对象可以获取到完整的消息信息,包含哪个broker,哪个队列,消息id是多少等等
@Override
public void onMessage(MessageExt messageExt) {
log.info("收到消息:{},抽取消息的内容:{}",messageExt,new String(messageExt.getBody()));
}
}
演示结果:
它会顺序接收该主题下的消息。
由于前面演示的发送分为了同步发送顺序和异步发送顺序消息,所以该主题下会有两个队列。
但是消费者接收时,单个队列下是严格按照消费点位顺序接收的。
只不过因为有两个队列,所以它轮询着从两个队列消费。
所以我的建议是在实际业务中,最好选择一个主题下只有一个队列。
另外我们注意到,通过同步发送的顺序消息,在消费时里面的结果也是顺序的。
但是通过异步发送的顺序消息,在消费时虽然是按照点位顺序消费的,但是对应的结果却不是顺序的。这就说明异步发送的顺序消息,在发送时就乱序了。(原因可能是网络波动或者并发量过大)
所以我们发送最好选择同步发送顺序消息。而且要想发送顺序性,一定要控制并发。最好保证单线程。
另外还有一点需要知道:某一消费者组消费完了消息,并不意味着该主题下的消息就不能继续被其他消费者消费了。也就是说即使某一消费者组消费完了消息,也不影响其他消费者组消费。只不过该消费者组中的任何消费者不能继续消费了而已。
6.4.2 消费者的两种模式
前面代码注释中也说明了,提供了两种消费者模式。即默认的集群模式(负载均衡)与广播模式。
负载均衡模式
就是多个消费者交替消费同一主题里面的消息。
举个例子:假如当前启动有三个消费者,它们同属于一个消费者组,那么在负载均衡模式下,它们就会交替去接收该主题下的消息。
广播模式
广播模式下,主题下的任意一条消息都会发给订阅该主题的所有消费者组,每个消费者组内初始化多少个消费者,就将这些消息都发送给这些消费者,每个消费者都会收到所有的消息。
简单说就是:广播模式下,每个消费者都会收到该主题所有的消息。不管它们是不是一个消费者组。
广播模式不会记录消费点位(其实严格说记录在本地),也不会重试,反正会发,但是不管你收不收得到。
举例说明:
某主题中有10条消息M1-M10,
共有两个消费者组CG1-CG2,它们的消费模式都是广播模式,同时都订阅了该主题。
CG1组下有3个消费者C1-C3,CG2消费者组内有4个消费者C4-C7。
那么CG1和CG2消费者组都收到了这10条消息。且各自组内的C1-C7也均收到了10条消息。
七、常见的消息问题
7.1 如何解决消息堆积问题
什么是消息堆积
某个主题下,某个消费者组中代理者位点和消费者位点的差值比较大。说明很多消息还没有被消费。
一般认为:单个队列的差值大于等于10w,就认为消息堆积了。(当然得是集群模式哈)
什么情况下会出现消息堆积
生产的速度超过消费的速度,就会造成消息堆积。
如果生产速度远超消费的速度,那么短期内就有可能造成大量消息堆积。
如何解决消息堆积问题
根据业务情况酌情考虑以下方面:
(一)检查是否是消费者问题,检查代码业务逻辑等。
(二)控制生产速度,比如对生产进行限流操作
(三)优化消费速度
(四)增加消费者数量。前提是消费者数量必须小于等于队列数量才有效
(五)如果增加消费者数量还不行,可以尝试单个消费者内部使用多线程的方式。
不过要注意的是线程池设置多少线程也很重要。
针对业务类型,如果业务是IO密集型,那么最大线程数推荐为2n,
如果是CPU密集型,那么最大线程数推荐设置为n+1,其中n表示设置的核心线程数。
(六)增加队列数量。增加了队列同时还可以增加消费者。这个一般不推荐修改增加队列。
(七)如果堆积了也不想处理这些消息了,可以选择跳过堆积。
不过尤其要慎重考虑,该操作不能回滚!
题外话
我们也可以重置消费点位,重新开始消费。
7.2 如何避免消息丢失问题
通常而言,我们说发送单向消息时,是可能存在消息丢失的问题的。而一般针对单向消息,我们也不会去做避免处理。
而针对同步消息和异步消息,都是需要broker确认的。broker会返回确认消息给生产者。如果发送失败可以进行重试发送。最终重试也失败的,生产者端可以做好日志或者数据库记录。这样可以明确知道哪些消息发送成功了,哪些消息发送失败了,降低消息丢失的风险。
RocketMQ要保证消息不丢失,提供了两种刷盘策略:同步刷盘和异步刷盘策略。
同步刷盘是指:当生产者发送同步消息到达broker后,先是存到缓冲区中(内存),然后持久化写入磁盘之后,再返回确认消息。此时生产者才可以发送下一条消息。
异步刷盘:当生产者发送同步消息存到缓冲区后,直接就返回确认消息了。然后再自行持久化到磁盘。这种方式相较于同步刷盘效率更高,但是存在消息丢失的可能性。RocketMQ默认的刷盘策略就是异步刷盘。
虽然前面说同步刷盘异步刷盘时说的是发送同步消息。其实发送异步消息也是一样。都会收到broker返回的确认信息。只不过异步消息不需要等到broker确认便可以接着发送。但是最终也会收到确认消息,是发送成功还是失败。
前面说了,如果是异步刷盘不能完全保证消息不丢失,如果写入缓冲区后,没来得及持久化服务器宕机了,那么就会造成这部分的数据丢失。
这种解决办法就是发送方和接收方都做好相应的记录。比如发送方记录每一条消息的发送状态,如果是自己发送过的,那么让状态设置为1。接收方每接收到一条消息,处理完后,将状态设置为2。这样就可以明确的知道哪些消息是丢失掉的。然后我们可以写个定时任务去将这些丢失的消息补发即可,前提要做好幂等性,防止重复补发。
当然,最后我们还可以主从同步,设置为必须从服务器也刷盘了,才返回确认消息。这样即使主服务器挂掉,从服务器也有数据。这种保障更加降低了消息丢失的风险。
以上就是避免消息丢失主要的方式。
当然了,至于顺序消息和定时消息这些,其实也都是同步发送或者异步发送的。同样遵循以上原则。
7.3 如何避免消息重复消费问题
7.3.1 什么情况下产生重复消息?
首先,我们必须要搞清楚一点,就是消息在什么情况下会产生重复消息。
(一)发送消息时就重复了
这种情况有可能是MQ收到生产者发送的消息,返回确认信息后,由于网络波动等原因导致生产者端没有收到MQ的响应信息。报了超时异常,然后生产者端发起重试,又发送了一条消息。
(二)消费者在消费完一条消息返回offset时,服务端突然宕机
此时,服务端如果重启,那么又会从之前的offset开始推送消息给消费者。就导致重复发送了。
7.3.2 如何解决重复消费的问题?
其实要解决重复消费的问题,关键在于幂等性。
而对于发送消息的重复,我们没必要考虑幂等,只要在消费端保证幂等就可以了。
所以完全可以在消费端解决重复消费的问题。比如判断发送的消息是否是同一条消息,可以通过唯一id(比如订单号)等方式去确认,如果订单号相同那么就是同一条消息。在业务逻辑中排除处理掉即可。
7.4 消息的重试和死信队列
生产者发送同步或者异步消息时,如果失败了会进行重试,重试次数可以自己设置。前面提到过如果设置。
消费者如果消费出现异常也会尝试重新消费。重试的次数默认是16次。
一旦超过重试次数,则该消息会进入死信队列中。
我们可以在写一个消费者让它消费死信队列中的消息。
八、offset提交方式
在集群模式下,消费者在消费完消息后会向Broker提交消费进度offset。提交方式分为同步提交和异步提交。
同步提交:消费者在消费完一批消息后向broker提交这些消息的offset,然后等待broker的成功响应,若在等待超时之前收到了成功响应,则继续读取下一批数据进行消费。若没有收到响应,则会重新提交,直到获取到响应。而在等待过程中,消费者是阻塞的,严重影响了消费者的吞吐量。
异步提交:消费者在消费完一批消息后向broker提交offset,但无需等待broker的成功响应,可以继续取并消费下一批消息。但是需要注意,broker在收到提交的offset后,还是会向消费者进行响应的。
所以这么来看,rocketmq无论那种提交方式,都可能造成重复消费。要解决重复消费,RocketMQ并没有帮我们做处理,就需要我们自己进行幂等性操作,处理好业务了。
个人认为,这一点上RocketMQ是落后Kafka的。Kafka里面分为了自动提交和手动同步、异步提交。Kafka手动同步提交的话则是一条消息offset确认提交ok才能消费下一条。
八、安全
我们发现前面springboot操作rocketmq进行发送消息消费消息的时候,是不是任何密码都没有输入?这样是不安全的。
接下来,就是配置密码了。
开启acl
进入配置文件目录conf,修改broker.conf配置文件,在配置后面追加
aclEnable=true
这样表示要安全登录才可以。于是就必须使用用户名密码进行登录了。
配置用户名密码
默认情况下,在conf配置文件目录下,有一个plain_acl.yml文件。这里面提供了默认的用户名和密码, 如果要修改,直接修改对应的配置即可。
我们现在不用它,自己来设置一个
springboot配置文件添加用户密码
# 如果服务器配置了安全登录,则需要对应的用户名密码才可以
rocketmq.producer.access-key=rocktetmq2
rocketmq.producer.secret-key=12345678
rocketmq.consumer.access-key=rocktetmq2
rocketmq.consumer.secret-key=12345678
同样,配置密码后,rocketmq可视化项目就不能启动了,如果要想启动,就得在它的配置文件中也配置上对应的用户名密码才行。这里就不一一演示了。