简介
RabbitMQ的持久化包含两个部分:队列索引(queue index)和消息存储(message store)。
队列索引负责维护消息在队列中的位置信息,以及消息的状态(消息是否投递给消费者或被消费者确认)。因此每个队列都会有一个自己的队列索引。消息索引则是一个全局的KV存储,RabbitMQ中所有的消息均存储在这里。
从3.5.0版本开始,小的消息直接存储在队列索引中而不是存储在消息存储中。具体通过queue_index_embed_msgs_below进行配置,默认值为4096字节。即消息内容、属性以及headers长度累加小于4096的将直接存储在队列索引中。
每个队列都有自己的队列索引,队列索引文件后缀名为idx。
消息直接存储在索引中的好处是:仅需进行一次写操作,而存储在消息存储中的消息则需要两次写操作(一次写索引,一次写消息存储),因此会有一定的性能提升。但另一方面需要注意的是:如果一个消息通过exchange同时路由到多个队列中,消息会被写到每个队列的索引文件中。而如果消息是写入消息存储中,仅仅只有一个副本。
内部实现概念
描述索引文件格式前,先简单说下内部实现中的一些概念词
1、entry(条目)
队列中的每个消息就是一个entry
每个消息包括三种状态:
publish: 消息已投递到队列中
deliver: 消息已投递给消费者
ack: 消息已被消费者确认
每个消息必然从publish到deliver再到ack经过三种状态,一旦消息被消费者确认,那么这个消息就可以不在队列索引中进行存储。
对于每个publish的消息包括
SequenceID:消息在队列中的序号, 按进入队列的先后顺序编号
MessageID:消息的唯一ID
Metadata:元数据信息(包括属性、headers)
Message:消息的完整内容(可选), 对于比较小的消息而言
而每个deliver,ack只需要记录消息的SequenceID即可
2、segment(段)
存储固定长度的消息,其个数为16384,每个segment有固定的编号,编号从0开始,这意味着:
0号segment 存储SequenceID为 0-16383 的消息
1号segment 存储SequenceID为 16384-32767 的消息
以此类推
索引文件格式
前面讲到了每个消息都有publish,deliver,ack三种状态,而每种状态都有具体对应格式,索引文件内容就是按这些格式进行存储
publish对应的存储格式:
这里有几点需要说明下:
1)持久化位:1表示持久化、0表示非持久化
2)消息序号:表示在对应idx文件中的序号(14位最大能表示的值为16384),存储时先根据消息的SequenceID 除16384,计算出应该存在哪个idx文件中,然后求余得到在idx文件中的序号,反之根据idx文件的序号加文件中的消息序号可计算出消息真正的SequenceID
3)载荷长度:也就是具体消息内容的长度
4)总长度:消息内容、属性、headers加在一起的总长度,但是如果消息是存储在message_store中,总长度计算为0
deliver/ack对应的存储格式:
索引文件示例
以一个真实的索引文件举例说明:
按照前面的讲解来分析文件
C007其二进制为1100000000000111,第一位1,表示是publish到队列的消息,第二位也为1表示是一条持久化的消息;中间16字节为消息的唯一ID;紧接着8字节0为消息的有效期;再接下来的4字节00000400,对应十进制1024,即消息内容的长度为1024;再接下来4字节全部为0,表示消息在message_store中存储(而实际queue_index_embed_msgs_below配置为512,符合预期)。
再后又是一条publish的消息(C008),该消息的实际内容长度为7字节(00000007),消息内容加属性以及headers的总长度为304字节(00000130)。
跳过304字节看到连续两个4007(二进制为0100000000000111),前缀为01,表示序号为00000000000111的消息已经deliver给消费者,并且已经被消费者确认。