消息持久化是
RabbitMQ
最为人津津乐道的特性之一,
RabbitMQ
能够在付出最小的性能代价的基础上实现消息的持久化,最大的奥秘就在于
RabbitMQ
多层消息队列的设计上。下面,本文就从
MessageQueue
的设计和消息在
MessageQueue
的生命周期两个方面全面介绍
RabbitMQ
的消息队列。
RabbitMQ完全实现了AMQP协议,类似于一个邮箱服务。Exchange负责根据ExchangeType和RoutingKey将消息投递到对应的消息队列中,消息队列负责在消费者获取消息前暂存消息。在RabbitMQ中,MessageQueue主要由两部分组成,一个为AMQQueue,主要负责实现AMQP协议的逻辑功能。另外一个是用来存储消息的BackingQueue,本文重点关注的是BackingQueue的设计。
在RabbitMQ中BackingQueue又由5个子队列组成:Q1、Q2、Delta、Q3和Q4。RabbitMQ中的消息一旦进入队列,不是固定不变的,它会随着系统的负载在队列中不断流动,消息的状态不断发生变化。RabbitMQ中的消息一共有5种状态:
a)Alpha:消息的内容和消息索引都保存在内存中;
b)Beta:消息内容保存在磁盘上,消息索引保存在内存中;
c)Gamma:消息内容保存在磁盘上,消息索引在磁盘和内存都有;
d)Delta:消息内容和索引都在磁盘上;
注意:对于持久化的消息,消息内容和消息索引都必须先保存到磁盘上,才会处于上述状态中的一种,而Gamma状态的消息只有持久化的消息才会有该状态。
BackingQueue
中的
5
个子队列中的消息状态,
Q1
和
Q4
对应的是
Alpha
状态,
Q2
和
Q3
是
Beta
状态,
Delta
对应的是
Delta
状态。上述就是
RabbitMQ
的多层队列结构的设计,我们可以看出从
Q1
到
Q4
,基本经历的是由
RAM
到
DISK
,再到
RAM
的设计。这样的设计的好处就是当队列负载很高的情况下,能够通过将一部分消息由磁盘保存来节省内存空间,当负载降低的时候,这部分消息又渐渐回到内存,被消费者获取,使得整个队列有很好的弹性。下面我们就来看一下,整个消息队列的工作流程。
引起消息流动主要有两方面的因素:其一是消费者获取消息;其二是由于内存不足,引起消息的换出到磁盘上(
Q1-.>Q2
、
Q2->Delta
、
Q3->Delta
、
Q4->Q3
)。
RabbitMQ
在系统运行时会根据消息传输的速度计算一个当前内存中能够保存的最大消息数量(
Target_RAM_Count
),当内存中的消息数量大于该值时,就会引起消息的流动。进入队列的消息,一般会按着
Q1->Q2->Delta->Q3->Q4
的顺序进行流动,但是并不是每条消息都一定会经历所有的状态,这个取决于当时系统的负载状况。
当消费者获取消息时,首先会从
Q4
队列中获取消息,如果
Q4
获取成功,则返回,如果
Q4
为空,则尝试从
Q3
获取消息;首先,系统会判断
Q3
队列是否为空,如果为空,则直接返回队列为空,即此时队列中无消息(后续会论证)。如果不为空,则取出
Q3
的消息,然后判断此时
Q3
和
Delta
队列的长度,如果都为空,则可认为
Q2
、
Delta
、
Q3
和
Q4
全部为空
(
后续说明
)
,此时将
Q1
中消息直接转移到
Q4
中,下次直接从
Q4
中获取消息。如果
Q3
为空,
Delta
不空,则将
Delta
中的消息转移到
Q3
中;如果
Q3
非空,则直接下次从
Q3
中获取消息。在将
Delta
转移到
Q3
的过程中,
RabbitMQ
是按照索引分段读取的,首先读取某一段,直到读到的消息非空为止,然后判断读取的消息个数与
Delta
中的消息个数是否相等,如果相等,则断定此时
Delta
中已无消息,则直接将
Q2
和刚读到的消息一并放入
Q3
中。如果不相等,则仅将此次读到的消息转移到
Q3
中。这就是消费者引起的消息流动过程。
下面我们分析一下由于内存不足引起的消息换出。消息换出的条件是内存中保存的消息数量
+
等待
ACK
的消息的数量
>Target_RAM_Count
。当条件触发时,系统首先会判断如果当前进入等待
ACK
的消息的速度大于进入队列的消息的速度时,会先处理等待
ACK
的消息。步骤基本上
Q1->Q2
或者
Q3
移动,取决于
Delta
队列是否为空。
Q4->Q3
移动,
Q2
和
Q3
向
Delta
移动。
最后,我们来分析一下前面遗留的两个问题,一个是为什么
Q3
队列为空即可认定整个队列为空。试想如果
Q3
为空,
Delta
不空,则在
Q3
取出最后一条消息时,
Delta
上的消息就会被转移到
Q3
上,与
Q3
空矛盾。如果
Q2
不空,则在
Q3
取出最后一条消息,如果
Delta
为空时,会将
Q2
的消息并入
Q3
,与
Q3
为空矛盾。如果
Q1
不空,则在
Q3
取出最后一条消息,如果
Delta
和
Q3
均为空时,则将
Q1
的消息转移到
Q4
中,与
Q4
为空矛盾。这也解释了另外一个问题,即为什么
Q3
和
Delta
为空,
Q2
就为空。
上述就是整个消息在
RabbitMQ
队列中流动过程。从上述流程可以看出,消息如果能够被尽早消费掉,就不需要经历持久化的过程,因为这样会加系统的开销。如果消息被消费的速度过慢,
RabbitMQ
通过换出内存的方式,防止内存溢出。