大家好,我是一个喜欢诗词的java研发赛亚人,感谢您的关注~ ┗( ▔, ▔ )┛。微信搜索【程序猿卡卡罗特】,后续有更多硬核文章哦~
今日诗词:
为君持酒劝斜阳,且向花间留晚照。
– [宋·宋祁]《玉楼春·风前欲劝春光住》
使用过RocketMq的同学都知道,消息(Message) 是通过 Producer 经过RocketMQ,然后Consumer通过订阅消息,从而获得Producer的消息的。
但对RocketMq内部消息存储结构可能不太了解,今天我们就来扒一扒RocketMQ消息内部存储的裤子。
举个栗子
下面是最常见的,生产者、消费者使用RocketMQ的代码。
生产者
public static void main(String[] args) throws Exception {
//1、创建一个DefaultMQProducer,需要指定消息发送组
DefaultMQProducer producer = new DefaultMQProducer("Test_Quick_Producer_Name");
//2、指定Namesvr地址
producer.setNamesrvAddr("localhost:9876");
//3、启动Producer
producer.start();
System.out.println("producer start...");
//4、构建消息
Message message = new Message(
"DEV_HC_TEST", //主题
"TagA", //标签,可以用来做过滤
"KeyA", //唯一标识,可以用来查找消息
"hello rocketmq".getBytes() //要发送的消息字节数组
);
System.out.println(message);
// 5、发送消息:指定超时时间,默认是3s
SendResult result = producer.send(message, 13000);
System.out.println(result);
//6、关闭producer
producer.shutdown();
}
消费者
public static void main(String[] args) throws Exception {
//1、创建DefaultMQPushConsumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("Test_Quick_Consumer_Name");
// 2、设置namesrv地址
consumer.setNamesrvAddr("localhost:9876");
// 3、设置监听topic
consumer.subscribe(
"DEV_HC_TEST", //指定要读取的消息主题
"*"); //指定要读取的消息过滤信息,多个标签数据,则可以输入"tag1 || tag2 || tag3"
//4、创建消息监听
consumer.setMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
try {
//获取第1个消息
MessageExt message = msgs.get(0);
//获取主题
String topic = message.getTopic();
//获取标签
String tags = message.getTags();
//获取消息
String result = new String(message.getBody(),"UTF-8");
System.out.println("topic:"+topic+",tags:"+tags+",result:"+result);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
//消息重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
//消息消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
System.out.println("start...");
// 5、启动消费监听
consumer.start();
}
几个问题
1、消息是发送给到RocketMQ 的NameServer 还是Broker?存在NameServer 中还是Broker中?
2、消息是存到哪里了?内存?磁盘?
3、消息的索引 key 是如何加快消息的查找的?
4、我们平时说,消费者从消费队列中消费消息,这个消费队列是内部是如何存储Message的?是存储了完整的Message数据吗?
5、Tag 是如何做到可以过滤消息的?
RocketMQ架构图
为了故事顺序发展下去,我们先来看看RocketMQ的架构图,以及一个Msg从Producer生产出来,到被Consuemer消费,是如何进行流转的。
什么?你已经知道了这个架构了? 那我再带给为回顾回顾趴~
我们以一条Message 生产到消费的流程捋一捋找个流程:
1、NameServer:
- 相当于是一个注册中心(类似ZK)用于保存Broker的信息(Broker 的集群信息、Topic路由信息…)。什么是Topic路由信息?下面会讲到。
- 可以集群部署,集群中的每个NameServer不会有数据交互
2、Broker:
- 也称为消息服务器,用于存储Message
- Broker 分为主节点和从节点。以上图架构图为例。Broker有两个集群,Master 和 Slave实现了读写分离。
- Broker 启动时,会向所有的NameServer注册自身的集群信息、自身消息的路由信息
3、Producer:消息生产者
- Producer启动是会从NameServer集群中随机选取一个NameServer获取 Broker 的信息(topic 信息、BrokerName,Broker的IP…),这些信息我们成为Topic 路由信息。
- 发送消息是直接发给 Broker的,并没有经过Nameserver了(已经拿到了Broker 的IP,为啥还要经过NameServer呢?)
- 发送消息时会根据 Message的Topic,从路由信息表(启动时从NameServer中获取了)中根据负载策略选择一个Broker主节点,将消息发给这个Broker主节点。
4、Consumer:消息消费者
- Consumer 启动时会从NameServer 中获取Broker的路由信息
- 启动后,定时从Broker中拉取消息进行消费(有同学就要问了,Consumer不是有PUSH模式吗,PUSH模式不是Broker消息到达后,主动推给Consumer吗?实际上,PUSH、PULL模式都是Consumer主动从Broker拉取消息的,只是PUSH这个单词会让人很受歧义。后续再写一篇文章扒一扒Conumer消费流程的底裤。 (*´・v・)
好了,相信读者已经大致知道了Msg 发送到消费的流程,下面我们重点讲解Msg在Broker是如何存储的。
等等,你想知道Broker的路由信息到底是啥玩意?好趴,那我们先来唠唠这个路由信息。
Producer路由信息
1、首先,我们常说Msg是存到Broker上的队列(注意这里打上了双引号)上的。
2、实际上我们可以认为,一个Topic会分配多个队列(为了负载考虑),每条消息只会存在一个队列中。
3、Producer 发消息时,首先要找到Broker,然后在根据Topic发到其中一个队列中。
4、实际上一个队列的多个Topic的队列是以数字命名的:0、1、2、3…,也就是常说的queueId(队列ID)
所以路由信息包括哪些信息呢?
- <Topic, BrokerName> :根据Topic找到对应的Broker集群,主节点的brokerId为0,从节点>0。集群有多个节点时,Producer只会发给Broker主节点。
- <BrokerName, IP>:根据brokerName找到对应的IP
- <Topic, List queueId> :Topic 对应多个queueId(根据负载均衡算法找到其中一个queueId)
- 在最新版本的RokcetMQ中一条消息默认有16个写队列,16个读队列
注意:实际上Broker内部并不是按照上面这种数据结构去定义Topic路由信息的,以上是为了讲解方便。
那有同学就要问了:卡卡罗特,那具体的源码里面Topic路由信息到底有哪些字段?等我更新…(欢迎各位爷评论区给我留言,看看有哪些好学的爷~)
Broker存储结构
下面是RocketMQ Broker的主要存储数据的目录:我们说的消息、索引,消费队列,Broker运行时的一些信息都会存到磁盘中。
有同学又要问了:卡卡罗特,你这不是windows的目录结构吗?RocketMQ是部署在Linux中的吗?怎么会是你这样。
这位好学的同学问得好:实际上RocketMQ在Linux中的存储目录结构也是这样的。我是将RocketMQ源码load到本地,然后在本地IDEA中启动的哦~
需要先启动 rocketmq-namesrv
项目、后启动rocektmq-broker
项目(当然需要配置一些数据)。
怎么配置?
等我。。。。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bkDVF6zD-1628648928650)(imgs\RocketMQ存储原理\MQ内部目录.png)]
- commitlog:producer发送的所有Message都存到这个目录下
- config:Broker运行期间一些配置信息,主要包括下列信息
- consumequeue:(消费队列)topic的队列(上文提到一个Topic会分配多个队列)
- index:消息索引文件,主要存储消息Key与Offset的对应关系
- abort:Broker启动时会生成一个该文件。该文件内容为空(可以看到大小为为0k)。
- Broker正常退出会删除该文件。非正常退出该文件还会存在。
- 作用:用于判断Broker是否正常退出
- checkpoint:文件检测点,存储commitlog文件最后一次刷盘时间戳、consumequeue最后一次刷盘时间、index索引文件最后一次刷盘时间戳
CommitLog
驼峰命名法看起来舒服一点。
我们知道Msg是先存到Broker的内存中的,之后才会刷到磁盘中进行存储,实际上就是存储到这个文件夹的。
注意:所有的消息(不分区Topic)的全部数据都是存到这个文件夹中的。(surprise~ ,那之前说的Topic存储到队列中是什么鬼?别着急嘛,接着往下看 (´・ᴗ・`))
- 该目录下存在多个文件,文件时用消息的偏移量命名的,我们称这个文件为MappedFile
- 每个MappedFile默认1G大小,某个MappedFile存满了,就生成一个新的MappedFile继续消息的存储
偏移量是什么意思?
我们说Message存到到CommitLog 目录下的MappedFile中,每个具体存到MappedFile中**哪里。**我们可以通过offset(偏移量)来定位到这条信息。
1、首先offset 是对整个CommitLog来说的一个全局offset,
2、通过offset查找消息的过程:
- 找到MappedFile:offset/1024*1025(1G) 我们就可以得出是第几个 MappedFile
- 找到对应的MappedFile后,通过offset % (1024*1024) 就知道了在MappedFile中的偏移量了
- 知道在MappedFile中存储的起始地址 + 消息的size 就得到了消息在MappedFile的结束地址
- 拿到起始地址,结束地址,就可以直接从 MappedFile中读取到一条Message了
消息存储哪些信息?
public class Message implements Serializable {
private String topic;
private int flag;
// tag、index、delevelLevel.. 属性都存在这个map 中
private Map<String, String> properties;
// 消息内容
private byte[] body;
// 如果是事务消息,有ID
private String transactionId;
}
Message扩展属性,最终存储的Msg
重点注意下面四个属性
public class MessageExt extends Message {
// 1、消息队列ID
private int queueId;
// 2、消息字节数
private int storeSize;
// 3、在队列中的偏移量
private long queueOffset;
private int sysFlag;
private long bornTimestamp;
private SocketAddress bornHost;
private long storeTimestamp;
private SocketAddress storeHost;
private String msgId;
// 4、commitLog中的偏移量
private long commitLogOffset;
private int bodyCRC;
private int reconsumeTimes;
private long preparedTransactionOffset;
}
实际上从这个方法可以看到具体存了哪些数据:
protected static int calMsgLength(int bodyLength, int topicLength, int propertiesLength) {
final int msgLen = 4 //TOTALSIZE
+ 4 //MAGICCODE
+ 4 //BODYCRC
+ 4 //QUEUEID
+ 4 //FLAG
+ 8 //QUEUEOFFSET
+ 8 //PHYSICALOFFSET
+ 4 //SYSFLAG
+ 8 //BORNTIMESTAMP
+ 8 //BORNHOST
+ 8 //STORETIMESTAMP
+ 8 //STOREHOSTADDRESS
+ 4 //RECONSUMETIMES
+ 8 //Prepared Transaction Offset
+ 4 + (bodyLength > 0 ? bodyLength : 0) //BODY
+ 1 + topicLength //TOPIC
+ 2 + (propertiesLength > 0 ? propertiesLength : 0) //propertiesLength
+ 0;
return msgLen;
}
1)TOTALSIZE:该消息条目总长度,4字节。
2)MAGICCODE:魔数,4字节。固定值0xdaa320a7。
3)BODYCRC:消息体crc校验码,4字节。
4)QUEUEID:消息消费队列ID,4字节。
5)FLAG:消息FLAG, RocketMQ不做处理,供应用程序使用,默认4字节。
6)QUEUEOFFSET:消息在消息消费队列的偏移量,8字节。
7)PHYSICALOFFSET:消息在CommitLog文件中的偏移量,8字节。
8)SYSFLAG:消息系统Flag,例如是否压缩、是否是事务消息等,4字节。
9)BORNTIMESTAMP:消息生产者调用消息发送API的时间戳,8字节。
10)BORNHOST:消息发送者IP、端口号,8字节。
11)STORETIMESTAMP:消息存储时间戳,8字节。
12)STOREHOSTADDRESS:Broker服务器IP+端口号,8字节。
13)RECONSUMETIMES:消息重试次数,4字节。14)Prepared Transaction Offset:事务消息物理偏移量,8字节。
15)BodyLength:消息体长度,4字节。
16)Body:消息体内容,长度为bodyLenth中存储的值。
17)TopicLength:主题存储长度,1字节,表示主题名称不能超过255个字符。
18)Topic:主题,长度为TopicLength中存储的值。
19)PropertiesLength:消息属性长度,2字节,表示消息属性长度不能超过65536个字符。
20)Properties:消息属性,长度为PropertiesLength中存储的值。
所以可以粗略认为该文件的MappedFile中,Msg是如下结构存储的:
MessageId
我们知道每个消息都有一个唯一的msgId,那么这个ID是如何保证唯一性的呢?
实际上MessageId由三部分组成:
- IP:Broker的IP
- 端口号:Broker的端口号
- 消息偏移量:CommitLog的偏移量
通过上述三个部分来保证唯一性。
所以:我们通过messageId能找到对应的msg原因是:
1、msgId 解析出上述三个部分:IP:Broker 的IP
2、通过offset找到MappedFile,找到该MappedFile 上的消息起始地址,读取该消息的 TOTALSIZE
得到结束地址就可以拿到消息了。
Config 文件夹
主要是一些延迟消息、topic信息、消费队列信息… (不是本次重点,暂不讨论)
ConsumeQueue文件夹
1、每一个Topic 都会在该文件夹中生成一个以Topic命名的文件夹
2、Topic 下级目录是 Topic的队列信息,其实就是我们说的消费队列
3、每个队列中有多个MappedFile(也成为MappedFile),每个文件最大1G
ConsumeQueue 中的MappedFile中存储每个Topic的如下信息:
- CommitLog offset:在CommitLog中的偏移量
- size:消息的总大小。其实就是CommitLog中的MappedFile的消息的
TOTALSIZE
- tag hashCode:消息的 Tag经过哈希函数得到的哈希值(主要是为了保证每个 Item是20字节)
1、单个ConsumeQueue文件中默认包含30万个条目,单个文件的长度为30w×20字节
2、单个ConsumeQueue文件可以看出是一个ConsumeQueue条目的数组
ConsumeQueueItem 如何查找消息?
1、找到对应的Item 的index,假设是2
2、找到index=2,得到 commitLog offset + size,然后去CommitLog文件夹找就可以得到其下的MappedFile的起始地址、结束地址,也就拿到了消息。
需知
1、消息的全量内容是存到CommitLog中,因为每个消息都有Topic属性,所有在ConsumeQueue文件夹下的其中一个队列会一条Item数据
2、实际上Consumer也是从ConsumeQueue 下根据Topic下的队列(0、1、2、3…)进行消息消费的
- 假设是消费者是集群模式:每个消费者只会订阅Topic下的部分队列
- 广播消费:每个消费者订阅Topic下的所有队列
3、Consumer 基于 Tag 过滤也是通过这个文件进行过滤的。
什么?如何进行消息的过滤?
给我留言,等我。。。。
Index
RocketMQ引入了Hash索引机制为消息建立索引,HashMap的设计包含两个基本点:Hash槽与Hash冲突的链表结构。
发送消息时可以指定索引:
Message message = new Message(
topic, //主题
tag, // 标签
key, // 索引
("hello rocketmq" + i).getBytes() //要发送的消息字节数组
);
// Message 的构造函数
public Message(String topic, String tags, String keys, byte[] body) {
this(topic, tags, keys, 0, body, true);
}
Index 内部存储结构如下:
包含三个部分:
- IndexHead:该IndexFile的统计信息
- beginTimestamp:该索引文件中包含消息的最小存储时间。
- endTimestamp:该索引文件中包含消息的最大存储时间。
- beginPhyoffset:该索引文件中包含消息的最小物理偏移量(commitlog文件偏移量)。
- hashslotCount:hashslot个数,并不是hash槽使用的个数,在这里意义不大。
- hash 槽:一个IndexFile默认包含500万个Hash槽,每个Hash槽存储的是落在该Hash槽的hashcode最新的Index的索引。
- index Item:默认一个索引文件包含2000万个条目
- hashcode:key的hashcode。
- phyoffset:消息对应的物理偏移量。
- timedif:该消息存储时间与第一条消息的时间戳的差值,小于0该消息无效。
- preIndexNo:该条目的前一条记录的Index索引,当出现hash冲突时,构建的链表结构。
新增数据
假设一条msg到达Broker,存到CommitLog后,如果指定了 key,就需要存到该结构中。
1、key 经过 hash函数,得到一个hash值,该hashValue % (500w),拿到哈希槽,哈希槽中存储了IndexItem的下标
2、得到IndexItem的下标,填充Index Item的四个属性数据(该位置有值,说明发生哈希冲突,采用开放地址法指定preIndexNo)
通过Index如何查找数据?
1、key -> hashMethod -> hash slot -> IndexItem
2、拿到IndexItem后,取 phyoffset ,即CommitLog的偏移量,就取得到数据了
发现没有,这种索引方式,有点像Mysql的非聚簇索引的检索方式。
什么?什么叫Mysql的非聚簇索引?
给我留言,等我。。。。
结语
相信各位爷读完以上内容后就能回答问题的几个答案了:ヾ(●´∀`●)
为了省事,我在把这几个问题搬过来。
1、消息是发送给到RocketMQ 的NameServer 还是Broker?存在NameServer 中还是Broker中?
2、消息是存到哪里了?内存?磁盘?
3、消息的索引 key 是如何加快消息的查找的?
4、我们平时说,消费者从消费队列中消费消息,这个消费队列是内部是如何存储Message的?是存储了完整的Message数据吗?
5、Tag 是如何做到可以过滤消息的?
往期文章回顾
我是一个喜欢诗词的java研发赛亚人。微信搜索【程序猿卡卡罗特】,解锁跟多硬核文章。