一.RocKetMQ介绍
RocKetMQ是阿里巴巴开源的分布西消息中间件,它具有低延迟、高吞吐量、高可用性和高可靠性等特点,设用于构建具有海量消息堆积和异步解耦功能的应用系统。
上述概念解释:
-
低延迟:RocKetMQ 提供了高性能的消息传递机制,能够实现低延迟的消息传递。这对于需要实时响应的应用场景非常重要。
-
高吞吐量:RocKetMQ 能够处理大量的消息并实现高吞吐量的消息传递。这使得它非常适合处理海量消息的应用场景,如大规模的数据处理、日志收集等。
-
高可用性:RocKetMQ 提供了高可用性的消息传递机制,通过支持主备模式和集群部署,确保消息的可靠传递和系统的高可用性。
-
高可靠性:RocKetMQ 采用了消息持久化和消息复制机制,确保消息的可靠传递和数据的安全性。即使在出现故障或网络中断的情况下,消息也能够被正确地传递和处理。
-
异步解耦:RocKetMQ 支持异步消息传递,可以将消息的发送和接收解耦,提高系统的灵活性和可扩展性。这使得应用系统能够更好地应对高并发和大规模的请求。
1.基本概念:
- 生产者(Producer): 也称消息发布者,是RocKetMQ用来构建并传输消息到服务端的运行实体
- 主题(Topic): 是RocKetMQ中消息传输的顶级容器,用于标识同一类业务逻辑的消息;是一个逻辑概念,并不是实际的消息容器
- 消息队列(MessageQueue):是消息存储和传输的实际容器,也是消息的最小存储单位
- 消息者:(Consumer): 也称消息订阅者,是RocKetMQ中用来接受并处理消息的运行实体
- 消费者组(ConsumerGroup): 是RocKetMQ中承载多个消费行为一致的消费者负载均衡分组,与消费者不同,消费者组是一个逻辑概念,可以同时订阅和接收多个主题的消息。消费者组是一组具有相同消费逻辑的消费者实例的集合,它们共同消费同一个主题的消息
- NameServer: 可以理解成注册中心,负责更新和发现Broker服务。在NameServer的集群中,NameServer之间是没有任何通信状态的,它是无状态的
- Broker: 消息的中转角色,负责消息的存储和转发,接受生产者产生的消息并持久化消息;当用户发送的消息被发送到Broker时,Broker会将消息转发到与之关联的Topic中,以便让更多的接收者进行处理
-
上图简介: 强烈建议画一遍
生产者会根据不同的业务,将消息发送到不同的Topoc中,存储到不同的Message中,最终由消费者进行消费,多个消费者组成消费者组,多个Broker组成Broker集群,每个Broker中包含多个Topic。
1.1如何理解nameServer
它就像一个大管家一样,充当了消息队列的路由中心,负责管理和维护消息队列的元数据信息,帮助生产者和消费者找到彼此,并确保消息的可靠传输。
-
注册和发现 Broker:当 Broker 启动时,它会向 NameServer 注册自己的信息,包括主题(Topic)和分区(Partition / MessageQueue)的信息。消费者(Consumer)可以通过 NameServer 获取 Broker 的信息,从而订阅和消费消息。
-
路由消息:当生产者(Producer)发送消息时,它会将消息发送到指定的 Topic,NameServer 会根据 Topic 的路由规则将消息路由到相应的 Broker 上。
-
心跳检测:NameServer 会定期向 Broker 发送心跳检测请求,以确保 Broker 的可用性。
1.2如何理解broker
可以将 Broker 理解为一个邮局,生产者(Producer)将消息发送到 Broker,消费者(Consumer)从 Broker 接收消息。Broker 负责接收、存储和传递消息,确保消息的可靠性和顺序性。
1.3应用场景:更好的理解概念
短信
- producer: 手机短信进程,就是生产者
- message: 包含了编辑的文本短信信息的消息对象
- consumer: 能够接收到消息的手机短信进程
抢红包
- producer: 发红包的用户手机客户端进程
- message: 红包个数 封装的已经包含金额的消息对象
- consumer: 抢红包用户手机客户端进程中,通过点击开红包,触发的代码片
注:RocKetMQ的安装和配置可自行查找相关文献
二.启动--以windows系统为例
- 进入mq解压后的bin目录下,进入cmd,运行以下命令启动nameServer和broker
- 注:再启动前保证自己的JDK环境变量已经配置好
- 执行:mqnamesrv.cmd 启动nameServer
- 执行:mqbroker.cmd -n localhost:9876 启动broker,连接指定的服务器
- rocketmq团队提供了一个查询rocket状态数据的仪表盘系统:rocket-dashboard,可自行进行下载,在cmd中执行对应的jar包即可
- 以下代码基于mq5.0版本
三.基于SpringBoot演示MQ基础代码示例
导入依赖:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
1.生产者代码示例
1.1同步发送
是最常用的方式,是指消息发送方发出一条消息后,会在收到服务端同步响应之后才发送下一条的通讯方式,可靠的同步传输被广泛应用于各种场景,如重要的通知消息、短消息通知等。
自我理解:等待消息返回后在进行下面的操作,应用于重要场景,“两端都应做到句句有回应,才能感情稳定”
官网图片:
- 代码示例:创建测试类,进行编写
@Test
public void sendTest1() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
//本次代码实现同步发送
//准备一个生产者对象,并对其分组命名
DefaultMQProducer producer = new DefaultMQProducer("sndTest1");
//连接nameserver,同启动broker时的 mqbroker.cmd -n localhost:9876的地址保持一致
producer.setNamesrvAddr("localhost:9876");
//生产者producer于nameServer建立连接
producer.start();
//将消息同步发送
for (int i=0;i<3;i++){
//创建一个消息对象,主题topic按业务起名即可,
//tag对消息进行在分类,RocketMQ可以在消费端对tag进行过滤。
//携带消息的body
Message message =
new Message("topic",i+"sndTest","同步发送消息".getBytes(StandardCharsets.UTF_8));
//通过 SendResult 对象获取发送结果和相关信息
SendResult send = producer.send(message);
//获取发送状态
System.out.println("发送状态:"+send.getSendStatus());
//通过 getMsgId 方法获取消息的唯一标识
send.getMsgId();
//通过 getMessageQueue 方法获取消息所在的主题、队列和 Broker 的信息
System.out.println("消息到达主题,队列,broker信息:"+send.getMessageQueue());
}
//关闭 Producer 的主要原因有以下几点:
//资源释放:关闭 Producer 可以释放占用的资源,包括网络连接、线程等。这样可以避免资源的浪费和占用。
//优雅退出:关闭 Producer 可以保证程序的正常退出。在关闭 Producer 之前,可以先发送一个特殊的消息,
// 通知 Consumer 停止消费,然后再关闭 Producer。这样可以避免消息丢失和消费者无法正常停止的问题。
//避免内存泄漏:如果不关闭 Producer,可能会导致内存泄漏问题。
// Producer 内部可能会缓存一些数据,如果不及时释放,可能会导致内存占用过高。
producer.shutdown();
}
- 启动MQ,执行代码后查看MQ图形化界面,查看主题和消息,则会出现以下内容代表执行成功
1.2异步发送
- 异步发送是指发送方发出一条消息后,不等服务端返回响应,接着发送下一条消息的通讯方式。
- 消息发送方在发送了一条消息后,不需要等待服务端响应即可发送第二条消息,发送方通过回调接口接收服务端响应,并处理响应结果。异步发送一般用于链路耗时较长,对响应时间较为敏感的业务场景。例如,视频上传后通知启动转码服务,转码完成后通知推送转码结果等。
代码示例:
@Test
public void sendTest2() throws MQClientException, RemotingException, InterruptedException {
//本次代码实现异步发送
//准备一个生产者对象,并对其分组命名
DefaultMQProducer producer = new DefaultMQProducer("sndTest2");
//连接nameserver,同启动broker时的 mqbroker.cmd -n localhost:9876的地址保持一致
producer.setNamesrvAddr("localhost:9876");
//生产者producer于nameServer建立连接
producer.start();
//消息发送失败的重试次数
producer.setRetryTimesWhenSendAsyncFailed(0);
//计数器
final CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
final int index = i;
Message message = new Message("Simple", "TagA", (i + "sndTest2").getBytes(StandardCharsets.UTF_8));
// 异步发送消息, 发送结果通过callback返回给客户端
producer.send(message, new SendCallback() {
//发送成功回调
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(index + "_消息发送成功"+sendResult);
countDownLatch.countDown();
}
//发送失败回调
@Override
public void onException(Throwable throwable) {
System.out.println(index + "_消息发送失败");
throwable.printStackTrace();
countDownLatch.countDown();
}
});
}
//异步发送,如果要求可靠传输,必须要等回调接口返回明确结果后才能结束逻辑,否则立即关闭Producer可能导致部分消息尚未传输成功
countDownLatch.await(5, TimeUnit.SECONDS);
producer.shutdown();
}
1.3单向模式发送
- 发送方只负责发送消息,不等待服务端返回响应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。
- 与同步发送在代码上的唯一区别就是发送没有返回结果
代码示例:
@Test
public void sendTest3() throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
//本次代码实现单向发送
//准备一个生产者对象,并对其分组命名
DefaultMQProducer producer = new DefaultMQProducer("sndTest3");
//连接nameserver,同启动broker时的 mqbroker.cmd -n localhost:9876的地址保持一致
producer.setNamesrvAddr("localhost:9876");
//生产者producer于nameServer建立连接
producer.start();
//将消息同步发送
for (int i=0;i<2;i++){
//创建一个消息对象,主题topic按业务起名即可,
//tag对消息进行在分类,RocketMQ可以在消费端对tag进行过滤。
//携带消息的body
Message message =
new Message("Simple3",i+"sndTest3","同步发送消息".getBytes(StandardCharsets.UTF_8));
//通过 SendResult 对象获取发送结果和相关信息
producer.sendOneway(message);
System.out.println(i+"_消息发送成功");
}
//关闭 Producer 的主要原因有以下几点:
//资源释放:关闭 Producer 可以释放占用的资源,包括网络连接、线程等。这样可以避免资源的浪费和占用。
//优雅退出:关闭 Producer 可以保证程序的正常退出。在关闭 Producer 之前,可以先发送一个特殊的消息,
// 通知 Consumer 停止消费,然后再关闭 Producer。这样可以避免消息丢失和消费者无法正常停止的问题。
//避免内存泄漏:如果不关闭 Producer,可能会导致内存泄漏问题。
// Producer 内部可能会缓存一些数据,如果不及时释放,可能会导致内存占用过高。
producer.shutdown();
}
2.消费者代码示例
- 拉模式--Pull: 消费者主动去Broker上拉取消息
- 推模式--Push: 消费者等待Broker把消息推送过来
怎么理解以上两种模式?
-
数据流向:推模式中,数据的流向是从数据的提供者向数据的接收者;而拉模式中,数据的流向是从数据的接收者向数据的提供者。
-
主动性和被动性:推模式中,数据的提供者主动发送数据,而数据的接收者被动接收数据;而拉模式中,数据的接收者主动请求数据,而数据的提供者被动提供数据。
-
控制权:推模式中,数据的提供者决定何时发送数据,而数据的接收者无法控制数据的发送频率和数量;而拉模式中,数据的接收者决定何时请求数据,而数据的提供者被动提供数据。
-
实时性:推模式适用于实时性要求较高的场景,因为数据的提供者主动发送数据,可以及时传递最新的数据;而拉模式适用于实时性要求不高的场景,因为数据的接收者可以根据自身需求灵活控制数据的获取。
-
灵活性:推模式在数据的接收方面较为被动,无法灵活控制数据的获取;而拉模式在数据的获取方面较为灵活,可以根据需求灵活控制数据的获取。
2.1推Push代码示例:
@Test
public void consumerTest1() throws MQClientException {
// 创建一个默认的消息消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("SimpleConsumer");
// 设置NameServer地址
consumer.setNamesrvAddr("localhost:9876");
// 订阅主题和标签,消费所有消息
consumer.subscribe("Simple", "*");
// 设置消息监听器
consumer.setMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
使用for循环遍历消息列表,处理每条消息
for (int i = 0; i < list.size(); i++) {
System.out.println(i + "_消费成功" + new String(list.get(i).getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//启动消费者
consumer.start();
System.out.println("启动成功");
while (true) ;
}
3.拉Pull模式示例:
在RocketMQ中有两种Pull方式,一种是比较原始Pull Consumer
,它不提供相关的订阅方法,需要调用pull方法时指定队列进行拉取,并需要自己更新位点。另一种是Lite Pull Consumer
,它提供了Subscribe和Assign两种方式,使用起来更加方便。
- 拉取完成后会返回拉取结果
PullResult
,PullResult中的PullStatus表示结果状态:
FOUND表示拉取到消息,NO_NEW_MSG表示没有发现新消息,NO_MATCHED_MSG表示没有匹配的消息,OFFSET_ILLEGAL表示传入的拉取位点是非法的,有可能偏大或偏小。如果拉取状态是FOUND,我们可以通过pullResult
的getMsgFoundList
方法获取拉取到的消息列表。最后,如果消费完成,通过updateConsumeOffset
方法更新消费位点。
3.1PullConsumer示例:
- 首先要理解:
- 设置Name Server的地址时,是用于获取Broker的信息
- Broker中,有多个Topic主题
- 每个主题中包含多个(MessageQueue)消息队列
- 注:此处不理解可以看基础概念处的图片
- 偏移量概念:
偏移量(Offset)是用来标识消息在消息队列中的位置的一个值。每个消息都有一个唯一的偏移量,用于表示消息在队列中的顺序。
可以将偏移量理解为消息队列中的一个索引,类似于数组中的下标。偏移量越大,表示消息在队列中的位置越靠后;偏移量越小,表示消息在队列中的位置越靠前。
偏移量的作用是用来记录消费者消费消息的进度。当消费者消费一条消息后,会将消费的偏移量记录下来。这样,在下一次消费时,消费者就可以从上次消费的偏移量开始继续消费,确保消息的顺序性和不重复消费。
通过偏移量,消费者可以知道自己消费到了哪个位置,从而实现消息的有序消费和断点续传的功能。
代码示例:
@Test
public void consumerPull() throws MQClientException {
//创建一个Pull Consumer对象,并指定一个唯一的消费者组名
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");
consumer.setNamesrvAddr("127.0.0.1:9876");
//定义一个Set的主题集合
Set<String> topics = new HashSet<>();
topics.add("Simple3");
//消费者要拉取的集合们
consumer.setRegisterTopics(topics);
consumer.start();
while (true) {
//获取每一个主题
consumer.getRegisterTopics().forEach(n -> {
try {
//拿到一个topic对应的messageQueue(消息队列)
Set<MessageQueue> messageQueues = consumer.fetchSubscribeMessageQueues(n);
messageQueues.forEach(l -> {
try {
//获取消息队列的偏移量--从内存获取
long offset = consumer.getOffsetStore().readOffset(l, ReadOffsetType.READ_FROM_MEMORY);
//没有获取到为-1,改为从队列获取
if (offset < 0) {
offset = consumer.getOffsetStore().readOffset(l, ReadOffsetType.READ_FROM_STORE);
}
//改为队列的最大偏移量
if (offset < 0) {
offset = consumer.maxOffset(l);
}
//兜底的
if (offset < 0) {
offset = 0;
}
// 拉取消息,参数分别为:队列、消息过滤表达式、起始偏移量、拉取的消息数量
PullResult pullResult = consumer.pull(l, "*", offset, 32);
System.out.println("循环拉取成功_" + pullResult);
//判断拉取消息的状态
switch (pullResult.getPullStatus()) {
//拉取成功,其他状况可以用不同的case去处理
case FOUND:
pullResult.getMsgFoundList().forEach(k -> {
System.out.println("消费成功_" + k);
});
}
} catch (MQClientException e) {
throw new RuntimeException(e);
} catch (RemotingException e) {
throw new RuntimeException(e);
} catch (MQBrokerException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
} catch (MQClientException e) {
e.printStackTrace();
}
});
}
}
3.2LitePullConsumer示例:
- Subscribe方式:随机获取一个消息队列
@Test
public void consumerListPull() throws MQClientException {
DefaultLitePullConsumer litePullConsumer =
new DefaultLitePullConsumer("LifePullConsumer");
litePullConsumer.setNamesrvAddr("localhost:9876");
//随机获取
litePullConsumer.subscribe("Simple3", "*");
litePullConsumer.start();
while (true){
List<MessageExt> poll = litePullConsumer.poll();
System.out.println("消息拉取成功");
poll.forEach(n->{
System.out.println("消息消费成功_" + n);
});
}
}
- Assign方式:指定获取一个消息队列
@Test
public void consumerListPull1() throws MQClientException {
DefaultLitePullConsumer litePullConsumer =
new DefaultLitePullConsumer("LifePullConsumer1");
litePullConsumer.setNamesrvAddr("localhost:9876");
litePullConsumer.start();
//获取主题中的消息队列集合
Collection<MessageQueue> messages = litePullConsumer.fetchMessageQueues("Simple3");
ArrayList<MessageQueue> messageQueues = new ArrayList<>(messages);
//将消息队列集合分配给消费者,表示消费者将从这些消息队列中消费消息
litePullConsumer.assign(messageQueues);
//设置消费者从指定消息队列的指定偏移量(20)开始消费消息。这里的偏移量表示消息在消息队列中的位置,从 0 开始计数
litePullConsumer.seek(messageQueues.get(0), 10);
while (true){
List<MessageExt> poll = litePullConsumer.poll();
System.out.println("消息拉取成功");
poll.forEach(n->{
System.out.println("消息消费成功_" + n);
});
}
}
终于写完了!!!! 后续会更新顺序消息 延迟消息 批量消息 事务消息等!!!