【RocketMq源码篇-04】rocketmq的普通消息详解(broker存储位置,集群同步情况)

RocketMq源码核心篇整体栏目


内容 链接地址
【一】环境搭建、基本使用、可视化界面 https://zhenghuisheng.blog.csdn.net/article/details/147481401
【二】rocketmq集群搭建(docker版-2主2从) https://zhenghuisheng.blog.csdn.net/article/details/154921615
【三】dashboard安装搭建和启动详解 https://zhenghuisheng.blog.csdn.net/article/details/155371854
【四】rocketmq的普通消息详解 https://zhenghuisheng.blog.csdn.net/article/details/155679428

如需转载,请附上链接: https://blog.csdn.net/zhenghuishengq/article/details/155679428

rocketmq的普通消息详解

一、Rocketmq的普通消息详解

前面文章已经完成了rocketmq的集群搭建和dashboard的可视化面板搭建,接下来这篇文章正式进入RocketMq的基本使用,本篇文章直接进入到Rocketmq的普通消息的详解。 本文基于 RocketMQ4.9.4(Dashboard 1.x),某些Dashboard 展示行为可能与 RocketMQ 5.x 不一致 。

在使用前,可以先看一下官网对普通消息的定义和使用场景--官网普通消息,先借助于官网对普通消息的定义:

  • 普通消息一般应用于微服务解耦、事件驱动、数据集成等场景,这些场景大多数要求数据传输通道具有可靠传输的能力,且对消息的处理时机、处理顺序没有特别要求

上面两段皆来自于官网对普通消息的描述,接下来对于开发者的我们,需要从更多不同的角度学习rocketmq的普通消息,了解其业务场景和使用场景

1,rocketmq和dashboard启动

环境咱们已经搭建好了,接下来就对这个可视化界面做一个详细的了解,首先需要启动rocketmq和dashboard,所以这个系列文章都优质密切的关系,因此需要详细的了解前面的文章。

首先启动rocketmq集群,直接通过compose启动,因此切换到 /usr/local/env/rocketmq/cluster 目录下, 然后启动rocketmq集群,以后台的方式启动

shell 复制代码
cd /usr/local/env/rocketmq/cluster
sudo docker compose up -d

随后切换到 /usr/local/env/rocketmq/cluster/bashboard 路径下,随后直接启动脚本即可,在上一篇文章中有配置

shell 复制代码
cd /usr/local/env/rocketmq/cluster/bashboard
sudo bash start-dashboard.sh

会在控制台直接出现启动成功的日志

shell 复制代码
检查是否有进程占用端口 8888 ...
检查是否已有 dashboard.pid ...
启动 RocketMQ Dashboard ...
启动成功!PID=6695
日志文件:/usr/local/env/rocketmq/cluster/bashboard/dashboard.log

随后浏览器打开可视化页面 **http://192.168.1.2:8888/#/**, 那么就代表着项目启动成功

在后续文中,就不会再对这个启动进行阐述。

2,生产消息

在完成rocketmq的启动和dashboard的启动之后,接下来就正式的讲解普通消息的基本使用,学习正常的发送消息和消费消息,在学习前,也可以再官网中看以下普通消息的生命周期流程,简单来说就是一下流程 生产者生产消息-->消息发送到服务端待消费-->消息被消费者发现然后消费消息,并提交消费结果-->消费结构提交,修改offset偏移量-->消息被覆盖删除

接下来通过原生代码对rocketmq的普通消息进行生产和提交,首先需要加入以下依赖

yml 复制代码
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.9.4</version>
</dependency>

2.1,生产者写入消息

随后创建一个 DefaultMQProducer 默认的生产者组对象,然后必须绑定nameserver地址,随后启动producer,再执行send发送10万条消息,每条消息在36*2=72字节,就是每条消息大概在0.07kb左右,包括内部的一些其他参数,每条消息大概在120-200个字节,所以10万条大概就是15m的数据量

java 复制代码
package com.zhs.study.source;

import cn.hutool.core.lang.UUID;
import com.google.common.collect.Lists;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @Author zhenghuisheng
 * @Date:2025/12/07 16:28
 */
public class ProducerDemo {
    public static void main(String[] args) throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
        // 1. 创建生产者(Producer Group 名称)
        DefaultMQProducer producer = new DefaultMQProducer("zhsProducerGroup");
        // 所有参数中,该参数必须绑定
        producer.setNamesrvAddr("192.168.1.2:9876");
        // 2. 启动 Producer(不启动会报错)
        producer.start();
        System.out.println("Producer 启动完成");
        // 4. 发送消息
        System.out.println("开始发送消息...");
        // 3. 构建消息对象
        List<Message> list = new ArrayList<Message>();
        for (int i = 0; i < 100000; i++) {
            String uuid = UUID.randomUUID().toString();
            uuid = uuid + uuid;
            Message msg = new Message("zhsTopicV1", "TagA", uuid.getBytes());
            list.add(msg);
        }
        List<List<Message>> partition = Lists.partition(list, 1000);
        for (List<Message> msg : partition) {
            producer.send(msg);
            System.out.println("消息批量发送完成");
        }
        // 5. 关闭 Producer
        producer.shutdown();
    }
}

需要注意的点是,官方明确批量拉取的数据量不能大于1m,这里涉及到batch缓冲区的大小,超过1m就会报错MessageIllegalException

执行完成之后,再看dashboard,可以发现在BrokerA集群中有10万条数据,并且master和slave都有存,每个master的broker大概5w条数据,但是这个并不准,因为我们没有干预这个topic,因此用官方的原话说就是:topic不存在的情况下,topic和他的队列只会在一个broker中创建,并且队列默认是创建4个

下文会对这个topic进行验证,也许是dashboard1.0的bug,在后续2.0中似乎已经被修复,但是现在研究的主要是4.9的版本,所以只能暂时先用1.0的版本

3,消息到底存在哪个broker

首先再次全部清空数据

shell 复制代码
sudo rm -rf /usr/local/env/rocketmq/cluster/logs/*
sudo rm -rf /usr/local/env/rocketmq/cluster/store/*

再次递归创建目录commitlog,consumequeue和index,这里的目录需要对应前面docker-compose.yml中的挂载路径,如果不存在前面的挂载路径可能会启动失败

shell 复制代码
sudo mkdir -p /usr/local/env/rocketmq/cluster/store/brokerA-master/{commitlog,consumequeue,index}
sudo mkdir -p /usr/local/env/rocketmq/cluster/store/brokerA-slave/{commitlog,consumequeue,index}
sudo mkdir -p /usr/local/env/rocketmq/cluster/store/brokerB-master/{commitlog,consumequeue,index}
sudo mkdir -p /usr/local/env/rocketmq/cluster/store/brokerB-slave/{commitlog,consumequeue,index}

对这些创建的目录给予权限,方便后续数据的增删改查

shell 复制代码
sudo chmod -R 777 /usr/local/env/rocketmq/cluster/store

随后执行关闭docker容器的命令,将之前正在运行的broker给全部终止,并且重新启动

shell 复制代码
sudo docker compose down
sudo docker compose up -d

对dashboard也重新启动,防止内部缓存的一些干扰性

java 复制代码
cd /usr/local/env/rocketmq/cluster/bashboard
sudo bash stop-dashboard.sh
sudo bash start-dashboard.sh

执行完成后再次进入可以发现集群数据为空,主题数据不存在

接下来测试一个新的主题,叫做zhsTopicV2,测试一下数据到底是进入了哪个broker,到底是brokerA还是brokerB,dashboard到底存不存在误导性

java 复制代码
for (int i = 0; i < 100000; i++) {
    String uuid = UUID.randomUUID().toString();
    uuid = uuid + uuid;
    Message msg = new Message("zhsTopicV2", "TagA", uuid.getBytes());
    producer.send(msg);
    System.out.println("消息发送完成 uuid = " + uuid);
}

消息消费情况如下,此时只开了消费者和生产者

java 复制代码
消息发送完成 uuid = 3472e88e-3502-4ef5-a28b-1ede892657333472e88e-3502-4ef5-a28b-1ede89265733
消息发送完成 uuid = 5cd14d5c-a4e5-4d72-b51a-b67c08325d2f5cd14d5c-a4e5-4d72-b51a-b67c08325d2f

随后在docker容器中查看消息到底存在哪个broker里面,首先进入brokerA-master的容器里面,发现啥也没有

shell 复制代码
sudo docker exec -it a0306e902466 /bin/bash
cd /home/rocketmq/store-a-master/commitlog

但是在brokerB-master中,可以发现数据全部进入了里面,commitlog也有数据

shell 复制代码
sudo docker exec -it 5ee67b299e2e /bin/bash
cd /home/rocketmq/store-b-master/commitlog

所以印证了上面那个,如果不干预topic的创建,那么topic会创建在nameserver的第一个注册进来的broker中,因此这里验证确实只在brokerB-master中,brokerA-master中不存在。所以这个dashboard界面只能当作参考,至少证明集群这些是链接成功的

在这需要对broker进行一个初步的总结:

  • 1,rocketMq中的topic只是一个虚拟的逻辑概念,真实存储数据的还是queue
  • 2,rocketmq中集群的全部broker,是一个分片概念,就是做一个水平扩容,一条消息也只会存在一个broker的一个queue中,这个是他的底层设计,这样有效解决一些堆积,重复消费等问题
  • 3,在使用自动创建topic和队列时,那么消息只会存在naveserver注册的第一个broker中(RocketMQ 的默认策略(TBW102 路由)),topic和queue只会创建在这个broker中。

如果想要topic分布在所有的broker上,需要手动执行命令

java 复制代码
mqadmin updateTopic -n namesrv:9876 -t zhsTopicV2 -c DefaultCluster -q 4 -r 4

4,消费者消费消息

上面已经完成了生产者的写入和broker得到变化,接下来主要是讲解rocketmq的消费者是如何消费,首先是编写消费的代码,其代码如下

java 复制代码
package com.zhs.study.source;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;

public class ConsumerDemo {
    public static void main(String[] args) throws Exception {
        // 1. 创建 Consumer(必须填写消费组名称)
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("zhsConsumerGroup");
        // 2. 指定 NameServer
        consumer.setNamesrvAddr("192.168.1.2:9876");
        // 3. 订阅 Topic 和 TagA
        consumer.subscribe("zhsTopicV2", "TagA");
        // 4. 注册消息监听器(并发消费)
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                System.out.println(
                        "收到消息:" + new String(msg.getBody()) +
                                ", queueId=" + msg.getQueueId() +
                                ", msgId=" + msg.getMsgId()
                );
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        // 5. 启动消费者
        consumer.start();
        System.out.println("Consumer 启动成功");
        System.in.read();
    }
}

执行完后,控制台打印的日志如下,可以看到消息内容,消息要被消费的队列id和消息msgId

java 复制代码
Consumer 启动成功
收到消息:04c83296-d2ca-4e4e-b418-34777e138c1d04c83296-d2ca-4e4e-b418-34777e138c1d, queueId=1, msgId=7F000001561422D8CFE02344A3518695
收到消息:7df0b623-2067-4920-8bb1-fba8523656197df0b623-2067-4920-8bb1-fba852365619, queueId=1, msgId=7F000001561422D8CFE02344A326866D
收到消息:11ba985e-987d-4828-ae54-29990f40908811ba985e-987d-4828-ae54-29990f409088, queueId=1, msgId=7F000001561422D8CFE02344A32F8675
...

由于是通过main方法来消费消息,所以需要使用下面这段来保证和dashboard的长连接,保证程序不退出,那么dashboard就能展示数据

java' 复制代码
System.in.read();

打开控制台的消费者页面,就可以看到这个刚刚创建的消费者组 zhsConsumerGroup ,这里可以证明的是消息已经完全被消费,但是存在的一定误导性,因为上面已经说过了消息只存在brokerB上面,brokerA上面并没有存数据的

消费者的消费情况如下

  • 上面的RETRY表示重试的队列,由于都是成功消费,所以暂时没有出现重试的broker和队列,此时的brokerOffset和consumerOffset都是0
  • 下面表示的是消费者的消费情况,上面已经分析了消息只会进入到brokerB对应的队列中,因此这里也会存在偏差,按理来说只有brokerB中的数据被消费

最后再查看一下集群的详情,可以发现BrokerA和BrokerB中,会显示今天生产消息的总数和消费消息的总数

5,集群同步情况

上面对普通消息有了初步的概念,接下来查看以下集群中同步的情况。上面已经说过由于是自动创建topic,所以只会在nameServer轮询的第一个机器中的第一个broker中创建topic和queue,因此目前是只有brokerB中有topic和queue,因此数据目前是只存储在brokerB中,brokerA中暂时没有。

接下来就是研究brokerB中集群同步的情况,brokerB也有一个主从复制的小集群,为了保证高可用和高性能,因此存在brokerB-master和brokerB-slave的两个结点。接下来主要就是对这两个节点进行研究,现在master节点是已经成功的进行了数据的生产和消费,现在就是对slave节点判断数据记录是否成功同步。

5.1,master和slave区别

在brokerB-slave结点的配置文件中,设置了brokerRole的角色为SLAVE,因此他就会主动的去拉取brokerB-master结点的数据,但是他不直接参与消费

在brokerB-master的配置文件中,设置了brokerRole的角色为ASYNC_MASTER ,说明在master结点中,会直接参与消息存储、消费、负载均衡 ,并且会异步将数据同步到 slave

接下来在dashboard控制台查看zhsTopicV2的信息,可以只有brokerA或者brokerB的详情,不会具体表面是master还是slave,在rocketMq设计阶段就已经表明,核心信息主要以master为主,slave不会直接参与master所执行的事情

因此可以得到以下信息,创建的queue和topic只会存在master结点上

角色 是否创建 queue Producer 是否发送 Consumer 是否消费
Master ✔ 创建 queue ✔ 会发送到 Master ✔ 只能从 Master 拉取
Slave ❌ 不创建 queue ❌ Producer 不会发送 ❌ Consumer 不会消费

5.2,salve同步master的情况

5.2.1,控制台查看同步情况

接下来直接在控制台验证主从同步的情况,进入集群页面,点击状态按钮查看状态

随后根据内容对比以下指标,可以发现同步时完全ok的

指标 Master Slave 结论
commitLogMaxOffset 12350000 12350000 完全一致(强一致)
commitLogMinOffset 0 0 ✔ 一致
dispatchBehindBytes 0 0 ✔ 无落后
remainHowManyDataToFlush 0 0 ✔ 无待刷盘数据
putMessageFailedTimes 0 0 ✔ 无失败
5.2.2,对比commitlog文件

上面这种方式时最常用的方式,但是对比commotlog时最精准的方式,根据之前docker-compose挂载的情况,以brokerB-master结点为例,可以得知这个这个store文件已经被挂载到了 /usr/local/env/rocketmq/cluster/store-a-master 目录下

shell 复制代码
- /usr/local/env/rocketmq/cluster/store/brokerA-master:/home/rocketmq/store-b-master

接下来先看brokerB-master的commitLog文件,先进入容器内部,再查看commitlog的情况,此时的commitlog的大小是00000000001073741824

shell 复制代码
sudo docker exec -it 5ee67b299e2e /bin/bash
cd /home/rocketmq/store-b-master/commitlog

接下来先看brokerB-slave的commitLog文件,先进入容器内部,再查看commitlog的情况,可以看到这个此时的commitlog的大小也是00000000001073741824,和master的一模一样,说明咱们的主从复制也是完全没问题的

shell 复制代码
sudo docker exec -it 3ffd9d8dc986 /bin/bash
cd /home/rocketmq/store-b-slave/commitlog

这里的00000000001073741824大于是10g的大小,不是实际大小,而是预分配大小,可以直接通过du查看这个commitlog的实际大小

6,总结

在此篇完整的讲解了消息的基本使用,也是对前面的集群的搭建接进行了一个反馈,验证了前面的集群的搭建是没有问题的,同时也验证了dashboard没有那么的靠谱,还是需要进入服务内部去观察数据到底是如何存储和消费的。

RocketMQ4.x对应的Dashboard 统计的是Nameserver 路由缓存 + 客户端上报的 metrics 的数量,不是 commitlog 实际落盘量,它不是 consumequeue 真实偏移,也不是每个 broker 的真实消息数量 。

相关推荐
小熊officer9 小时前
RocketMQ简介
rocketmq
TracyCoder1235 天前
RocketMQ技术原理简单解析:从架构到核心流程
架构·wpf·rocketmq
zzhongcy5 天前
RocketMQ、Kafka 和 RabbitMQ 等中间件对比
kafka·rabbitmq·rocketmq
小股虫5 天前
RocketMQ消息可靠性实战:从发送到消费的全流程保障
rocketmq
u***u6855 天前
后端在消息队列中的可靠性保证
swiftui·ar·rocketmq
milanyangbo5 天前
从硬盘I/O到网络传输:Kafka与RocketMQ读写模型及零拷贝技术深度对比
java·网络·分布式·架构·kafka·rocketmq
Mr.朱鹏5 天前
RocketMQ可视化监控与管理
java·spring boot·spring·spring cloud·maven·intellij-idea·rocketmq
蜂蜜黄油呀土豆5 天前
RocketMQ 详解:从异步解耦到存储与消费全链路解析
消息队列·rocketmq·分布式账本·分布式系统·幂等设计
Mr.朱鹏6 天前
RocketMQ安装与部署指南
java·数据库·spring·oracle·maven·rocketmq·seata