RocketMQ基础学习
RocketMQ介绍
- RocketMQ 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式的特点。支持事务消息、顺序消息、批量消息、定时消息、消息回溯等。
名词解释
- 生产者(producer): 负责生产消息。RocketMQ通过负载均衡模块选择相应的broker集群队列进行消息投递,投递过程支持快速失败并且低延迟。RocketMQ中的生产者都是以生产者组的形式出现,同一生产者组发送相同topic类型的消息。而一个生产者组是可以同时发送多个主题的消息。
- 消费者(consumer): 从主题中获取消息的对象。一个消费者同时只能消费一个主题的消息。RocketMQ中的消费者都是以消费者组的形式出现。同一组消费者组消费的是同一个topic的消息。同一消费者组的消费者数量应该小于订阅的topic中queue数量(一个queue中的消息只能被一个消费者消费),超出的消费者不能消费任何消息。
- 主题(topic): 一类消息的集合。一个主题可以有多条消息,但是每条消息只能属于一个主题。一个topic可以被多个消费者组同时消费。
- 消息(message ): 所传输数据的物理载体,生产和消费数据的最小单位,每条消息必须属于某一个主题(topic)。
- 标签(tag): 为消息设置标签,用于同一主题下区分不同类型的消息。如果说主题是一级分类,则标签就相当于二级分类。
- 队列(queue): 存储消息的物理实体,一个topic中可以有多个queue,queue存储的就是该topic的消息。queue中的消息只能被一个消费者组中的一个消费者消费。
- 注册中心(NameServer): 是一个broker与topic路由的注册中心,支持Broker的动态注册与发现。负责broker和路由信息管理。接受broker集群的注册信息并且保存下来作为路由信息的基本数据,提供心跳检测机制。每个NameServer中都保存着broker集群的整个路由信息和用于客户端查询的队列信息。producer和consumer通过NameServer可以获取整个broker集群的路由信息,从而进行消息的投递和消费。
- 消息中转(broker): 负责存储消息、转发消息。接收并存储生成这发送的消息,同时为消费者拉取请求做准备。
工作流程
-
- 启动NameServer,开始监听端口,等待Broker、Producer、Consumer连接。
-
- 启动Broker时,Broker会与所有的NameServer建立长连接,然后每30秒向NameServer定时发送心跳包。
-
- 发送消息。可以在发送消息前手动创建Topic,并指定存储的Broker,然后将关联信息写入NameServer。也可以在发送消息时自动创建Topic。
-
- Producer发送消息。启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取路由信息(当前发送的Topic消息的Queue与Broker的地址映射关系)。选择一个Queue,与队列所在的Broker建立长连接,从而向Broker发消息。
-
- Consumer读取消息。启动时先跟NameServer集群中的其中一台建立长连接,获取其所订阅的Topic路由信息,然后根据路由信息获取需要消费的Queue,跟Broker建立长连接,开始消费其中的消息。
RocketMQ消息模型![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/db0c526ef84743a6be995809f834d594.png)
相同的ConsumerGroup下的消费者主要有两种负载均衡模式,即广播模式,和集群模式(图中是最常用的集群模式)。
- 集群模式: 同一个 ConsumerGroup 中的 Consumer 实例是负载均衡消费,如图中 ConsumerGroupA 订阅 TopicA,TopicA 对应 3个队列,则 GroupA 中的 Consumer1 消费的是 MessageQueue 0和 MessageQueue 1的消息,Consumer2是消费的是MessageQueue2的消息。
- 广播模式: 同一个 ConsumerGroup 中的每个 Consumer 实例都处理全部的队列。需要注意的是,广播模式下因为每个 Consumer 实例都需要处理全部的消息,因此这种模式仅推荐在通知推送、配置同步类小流量场景使用。
RocketMQ工作原理
1.消息的生产
-
- Producer发送消息之前,会先向NameServer发出获取消息Topic的路由信息的请求。
-
- NameServer返回该Topic的路由表(Map结构,key为Topic名称,value是一个QueueData实例列表。一个Broker中该Topic的所有Queue对应一个QueueData。)及Broker列表(Map结构,key为brokerName,value为BrokerData。一个Topic可能存在多个Broker,一个Broker也可能存在不同Topic的Queue)。
-
- Producer根据代码中指定的Queue选择策略,从Queue列表中选出一个队列,用于后续存储消息。
-
- Produer对消息做一些特殊处理,例如,消息本身超过4M,则会对其进行压缩。
-
- Producer向选择出的Queue所在的Broker发出RPC请求,将消息发送到选择出的Queue。
2.Queue选择算法
- 轮询算法: 默认选择的算法,每个Queue中可以均匀的获取到消息。但是因为一些原因,在某些Broker上的Queue可能投递延迟严重,从而导致Producer的缓存队列中出现较大的消息积压,影响消息的投递性能。
- 最小投递延迟算法: 每次投递消息,会统计上一次的延迟时间,将消息投递给时间延迟最小的Queue中,延迟时间相同则采用轮询算法。但是该算法会导致延迟小的Queue存在大量的消息,从而使该Queue的Consumer压力增大,降低了消息的消费速度,是MQ中的消息堆积。
3.消息写入
消息进入Broker后到持久化的过程
- Broker根据queueId获取到该消息对应索引条目要在consumequeue目录中的写入偏移量(queueOffset)。
- 将queueId、queueOffset等数据与消息一起封装为消息单元。
- 将消息单元写入到commitlog。(commitlog目录中存放着很多的mappedFile文件,mappedFile文件大小为1G。文件名由20位十进制数构成,表示当前文件的第一条消息的起始位移偏移量。当第一个文件放满时,则会自动生成第二个文件继续存放消息)
- 同时形成消息索引条目。
- 将消息索引条目分发到相应的consumequeue中。
4. 消息拉取
- Consumer获取到其要消费消息所在Queue的消费偏移量offset,计算出其要消费消息的offset(消费offset即消费进度,consumer对某个Queue的消费offset,即消费到了该Queue的第几条消息的消息offset = 消费offset + 1)。
- Consumer向Broker发送拉取请求,其中会包含其要拉取消息的Queue、消息及消息offset。
- Broker计算在该Consumequeue中的queueOffset(queueOffset = 消息offset*20字节)。
- 从该queueOffset处开始向后查找第一个指定Tag的索引条目。
- 解析该索引条目的前8个字节,即可定位到该消息在commitlog中的commitlog offset。
- 从对应commitlog offset中读取消息单元,并发送给Consumer。
实例
下载并配置好RocketMQ,启动nameserver注册中心(在rocketmq文件的bin目录下,进入cmd使用如下命令:start mqnamesrv.cmd)和Broker消息中转(start mqbroker.cmd -n 127.0.0.1:9876 -c 【MQ安装路径】/conf/broker.conf)后,才能正常使用。
- springboot项目中添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
- 生产者
import org.apache.rocketmq.client.producer.DefaultMQProducer;
public class Producer {
public static void main(String[] args) throws Exception {
// 创建生产者,并指定组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 指定Namesrv地址
producer.setNamesrvAddr("localhost:8888");
// 启动生产者
producer.start();
// 创建消息,指定Topic、Tag和消息体
String message = "Hello, RocketMQ!";
producer.sendOneway("TopicTest", "TagA", message.getBytes());
// 批量发送消息(批量消息的Topic必须相同)
//List<Message> messages = new ArrayList<>();
//messages.add(new Message("sanyouTopic", "java日记 0".getBytes()));
//messages.add(new Message("sanyouTopic", "java日记 1".getBytes()));
//messages.add(new Message("sanyouTopic", "java日记 2".getBytes()));
// 发送消息并得到消息的发送结果
//SendResult sendResult = producer.send(messages);
// 关闭生产者
producer.shutdown();
}
}
- 消费者
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
public class Consumer {
public static void main(String[] args) throws Exception {
// 创建消费者,并指定组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 指定Namesrv地址
consumer.setNamesrvAddr("localhost:8888");
// 订阅Topic和Tag
consumer.subscribe("TopicTest", "*");
// 设置回调函数来处理消息
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (org.apache.rocketmq.common.message.Message msg : msgs) {
System.out.println("Received message: " + new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者
consumer.start();
}
}