阶段一、阶段二讲的都是 RabbitMQ 的「功能层」——路由、模型、可靠性。从这一篇开始,下沉到「实现层」,看消息在 RabbitMQ 内部到底是怎么存储和流转的。这是理解 RabbitMQ 性能、可靠性、容量边界的根基。

第一个要拆的是经典队列(Classic Queue)。它是 RabbitMQ 最老的队列类型,也是理解其他队列类型(Quorum、Stream)的参照系。即便 RabbitMQ 4.0 之后官方主推 Quorum Queue,经典队列依然是最常用的单机队列类型,理解它的存储机制,才能明白后续类型为什么要那么设计。

经典队列的存储分水岭:内存 vs 磁盘

经典队列存储消息,本质上是在内存和磁盘之间做分配。一条消息进队列后,它的去向有两种可能:

  • 留在内存:访问快,但占 RAM。broker 重启或崩溃,内存里的消息丢失(除非持久化后又落盘)。
  • 写磁盘:不占 RAM,但读写慢。持久化的消息必须在磁盘有一份,才算可靠。

经典队列的存储策略,就是决定「哪些消息留内存、哪些写磁盘、什么时候做这个转换」。这个决策受三个因素影响:消息是否声明了持久化(deliveryMode)、队列的内存水位、是否声明为惰性队列(lazy)。

持久化消息与非持久化消息的不同命运

先看最基础的区分——消息是否持久化

  • 持久化消息(deliveryMode=2):进队列时会先写磁盘(写进消息存储),然后再考虑要不要放一份在内存里做缓存。broker 崩溃后能从磁盘恢复。写盘带来延迟,但保证不丢。
  • 非持久化消息(deliveryMode=1):进队列时只放内存,不写磁盘。访问快,但 broker 重启就全丢。

持久化消息虽然必须写盘,但为了消费时的速度,RabbitMQ 会尽量在内存里保留一份缓存。所以持久化消息实际上是「磁盘 + 内存缓存」双份,消费优先从内存读,内存没有再读盘。

内存水位的治理

队列不能无限往内存塞消息——内存有限,塞满了 broker 就会触发内存告警,阻塞生产者甚至崩溃。RabbitMQ 用一套**内存水位(memory watermark)**机制来管理:

  • broker 设一个内存阈值(vm_memory_high_watermark,默认是物理内存的 0.4)。
  • 当 broker 内存使用逼近这个阈值,队列开始把内存里的消息页出到磁盘(page out),腾出内存。
  • 页出是按队列进行的,每个队列维护自己的内存索引。

这个「内存满了往磁盘挪」的过程,会带来一个性能现象:队列积压少时(消息都在内存),消费很快;积压多了(触发页出),消费速度骤降,因为要从磁盘读。这就是经典队列「积压到一定量级后性能断崖」的根因。

这个阈值(0.4)不能随便调高。调高意味着允许更多内存给消息缓存,但同时留给 broker 自身(Erlang VM、其他队列、连接处理)的内存更少,更容易 OOM。调低则页出更频繁,磁盘 IO 压力大。0.4 是官方反复验证的平衡点,一般不动。

3.12 的关键变化:默认行为转向惰性

经典队列的存储策略,在 RabbitMQ 3.12 经历了一次重要转向。3.12 之前,经典队列默认把消息尽量放内存,靠页出机制在内存满时写盘——这种策略吞吐高,但大队列积压时内存压力大,页出导致的延迟抖动明显。

3.12 之后,经典队列的默认行为变得更像惰性队列(lazy queue)

  • 消息进来后更倾向于直接写磁盘,内存里只保留少量最近的消息做缓存。
  • 不再像以前那样把大量消息堆在内存里等页出。
  • 内存占用显著降低,大队列更稳定,不再有「积压到一定量级后内存爆炸」的问题。
  • 代价是:小队列(消息很快被消费、本来不积压的)的吞吐略有下降,因为多了写盘开销。

这次变化的背景是:官方发现生产环境大量的事故来自经典队列的内存问题(OOM、页出抖动),而不是吞吐不够。所以宁可牺牲一点小队列的吞吐,换取大队列的稳定性。这是典型的「为可靠性牺牲峰值性能」的取舍。

理解这个变化的意义:在 3.12+,显式声明 lazy queue 的边际收益变小了——因为默认行为已经接近 lazy。但如果你有那种「确定会积压百万级消息」的队列,显式 lazy 仍然有意义,能让存储行为更确定。

经典队列的核心数据结构

经典队列在内部用 Erlang 的数据结构存消息。简化的理解:

  • 每个队列维护一个消息索引(在内存里),记录每条消息的元数据(ID、位置、状态)。
  • 消息体本身根据策略放内存或磁盘。
  • 队列按 FIFO 顺序遍历索引,投递消息给消费者。

这个「索引在内存、数据可盘可内存」的设计,决定了经典队列的特点:只要索引在内存(哪怕消息都在盘上),队列就能正常工作。但如果索引本身太大(积压几百万条消息,索引占了几 GB 内存),内存压力就来了。这也是为什么大队列最终还是会吃内存——索引躲不开。

经典队列该用在什么场景

经典队列依然有它的位置。判断该不该用它,看场景:

适合经典队列

  • 单机部署、不需要高可用副本(队列只在一个节点)。
  • 对吞吐和低延迟有要求,且队列不会大量积压。
  • 消息可以容忍单节点故障丢失(非核心业务),或者配合持久化 + 不停机重启的运维手段。

不适合经典队列

  • 需要高可用(节点挂了消息不能丢)→ 用 Quorum Queue。
  • 需要消息回放 / 多消费者独立读 → 用 Stream。
  • 队列会大量积压、内存敏感 → 考虑显式 lazy 或 Quorum。

核心判断:经典队列是「单机、无副本」的队列。它不解决高可用,只解决单机内的消息存储。需要高可用的场景,必须上 Quorum Queue 或 Stream——这是接下来两篇的主题。

经典队列的内存与磁盘取舍

收束:经典队列是理解其他队列的参照系

经典队列的存储机制——内存与磁盘的取舍、内存水位治理、3.12 的行为转向——是理解 RabbitMQ 存储的基础。Quorum Queue 和 Stream 都是在经典队列暴露的问题(单点故障、内存压力)上做的重新设计。

下一篇继续讲存储的实现细节——消息在 Erlang 里具体怎么持久化、ETS 和消息存储的关系,那是经典队列存储的底层机制。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。我相信 AI 是程序员的最佳搭档。想跟完这套「图解 RabbitMQ」,欢迎关注公众号 「十三Tech」

十三Tech公众号二维码