Files
cs-note/hhs/MQ/04-存储引擎/11-RabbitMQ-消息存储.md
T
2026-05-24 20:51:06 +08:00

150 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [MQ, RabbitMQ, 存储引擎, Erlang]
create time: 2026-05-24 19:52
---
# RabbitMQ 消息存储
## 概述
RabbitMQ 基于 Erlang/OTP 平台构建,消息存储依赖 Erlang 进程模型和 Mnesia 分布式数据库。本文深入分析 RabbitMQ 的消息持久化机制、队列类型演进(经典队列、仲裁队列、流式队列)、内存管理策略,以及消息从写入到删除的完整生命周期。
## 正文
### 1. 存储架构:Erlang 进程 + Mnesia
RabbitMQ 的每个 Queue 本质上是一个 Erlang 进程(gen_server),拥有独立的 mailbox 和状态。这种设计天然隔离了不同队列的故障,但也意味着每个队列的资源消耗(内存、调度时间)受到 Erlang 虚拟机(BEAM)的约束。
持久化元数据(Exchange 定义、Binding 关系、用户权限等)存储在 **Mnesia** 分布式数据库中。Mnesia 是 Erlang 原生的数据库,支持事务和分布式复制,但它的设计目标是存储小量元数据,而非大规模消息数据。实际的消息体存储由各队列进程自行管理。
```mermaid
graph TD
Producer["Producer"] -->|"AMQP 发布"| Exchange["Exchange"]
Exchange -->|"路由规则"| Binding["Binding"]
Binding --> Queue1["Queue 进程 1"]
Binding --> Queue2["Queue 进程 2"]
Queue1 -->|"持久化写入"| MsgStore["消息存储"]
Queue1 -->|"内存缓存"| Memory["进程内存"]
MsgStore --> Disk["磁盘文件"]
Queue1 --> Consumer["Consumer"]
style Exchange fill:#4A90D9,color:#fff
style Queue1 fill:#F5A623,color:#fff
style Queue2 fill:#F5A623,color:#fff
style MsgStore fill:#6EC1E0,color:#fff
```
### 2. 消息持久化机制
RabbitMQ 中消息要真正持久化,需要同时满足两个条件:
- **Queue 声明为 durable**:队列的元数据在 Broker 重启后保留。
- **Message 的 delivery mode 设为 2(persistent)**:消息体写入磁盘。
> [!question] durable 和 persistent 有什么区别?
>
> `durable` 是队列的属性,控制的是"队列本身在重启后是否还存在";`persistent` 是消息的属性,控制的是"这条消息是否需要写入磁盘"。两者必须同时设置才能实现真正的持久化。如果只有 durable 没有 persistent,队列重启后还在,但里面的消息会丢失;反过来则队列重启后直接消失,消息也就无从谈起了。
消息写入磁盘的时机并非立即的。RabbitMQ 使用一种"惰性"写入策略:消息先写入内存,当满足一定条件(如内存压力达到阈值、或显式 flush)时才批量刷入磁盘。这意味着在极端情况下(如 Broker 突然崩溃),少量消息可能丢失。
### 3. Queue 类型演进
RabbitMQ 3.8+ 引入了三种队列类型,各自面向不同的可靠性与性能需求:
**经典队列(Classic Queue)**是 RabbitMQ 最早的队列实现。消息存储在 Erlang Mnesia 表中(实际上从 3.x 开始使用自定义的文件存储引擎),支持可选持久化。单 Master 架构,不支持复制。在消息堆积时性能急剧下降,因为队列进程需要维护一个按 offset 索引的磁盘文件结构,大量消息会导致 GC 压力和 I/O 放大。
**仲裁队列(Quorum Queue)**基于 Raft 共识协议,每个队列在多个节点上维护副本(奇数个,默认 3 个)。消息写入需要多数节点确认,天然保证了数据安全。底层使用 WAL(Write-Ahead Log)顺序写盘,性能在大量消息堆积时保持稳定。
**流式队列(Stream)**是 3.9 引入的新类型,借鉴了 Kafka 的设计思想。消息以 append-only log 形式存储,支持多消费者独立读取(非竞争消费),消息不会被消费后删除,支持回溯重放。适合事件驱动、审计日志等场景。
| 特性 | Classic Queue | Quorum Queue | Stream |
|------|:---:|:---:|:---:|
| 复制 | 不支持 | Raft 多副本 | 可选 |
| 持久化 | 可选 | 必须 | 必须 |
| 消费后删除 | 是 | 是 | 否 |
| 消息回溯 | 不支持 | 不支持 | 支持 |
| 适合场景 | 简单队列 | 高可靠 | 事件流 |
> [!question] 经典队列在大量消息堆积时性能为什么会急剧下降?
>
> 经典队列的存储引擎在消息堆积时面临两个瓶颈:一是 Erlang 进程的 mailbox 膨胀导致调度延迟增大,GC 暂停时间变长;二是磁盘存储使用了按消息 ID 索引的文件结构,大量随机 I/O 使得写入性能断崖式下跌。仲裁队列通过 WAL 顺序写解决了 I/O 问题,通过 Raft 副本机制分散了单节点压力,从而在堆积场景下保持稳定。
### 4. Lazy Queue:磁盘优先策略
Lazy Queue 是经典队列的一种特殊模式,将消息尽可能早地写入磁盘,只在消费者需要时才加载到内存。适合消息堆积量大但消费速度跟不上的场景。
配置方式:声明队列时设置 `x-queue-mode: lazy`。在 Quorum Queue 中,由于 WAL 本身就是顺序写磁盘的,不需要额外的 Lazy 模式。
### 5. 内存管理:告警、流控与信用机制
RabbitMQ 的内存管理是一个三层防护体系:
**内存告警(Memory Alarm)**:当节点内存使用超过阈值(默认物理内存的 40%),Broker 触发内存告警,阻塞所有生产者的消息发布,直到内存回落到安全水位。
**流控(Flow Control)**:当某个 Erlang 进程(如队列进程)的消息积压超过一定阈值,会主动向 TCP 连接进程发送 `pause` 信号,让生产者的 TCP 接收窗口降为 0,从而在协议层面实现背压。
**信用机制(Credit Flow)**:这是 Erlang 进程间的流控机制。每个进程维护一个 credit 计数器,消息传递消耗 credit,消费端处理完毕后归还 credit。当 credit 归零时,发送端阻塞,防止过快地往下游进程灌消息。
```mermaid
graph TD
Producer["Producer"] -->|"发送消息"| Conn["连接进程"]
Conn -->|"credit 检查"| Queue["队列进程"]
Queue -->|"内存检测"| MemCheck{"内存超阈值?"}
MemCheck -->|"是"| Alarm["触发内存告警"]
Alarm -->|"阻塞生产"| Producer
MemCheck -->|"否"| Process["正常处理"]
Process --> Consumer["Consumer"]
style Alarm fill:#D0021B,color:#fff
style Queue fill:#F5A623,color:#fff
```
### 6. 消息的生命周期:写入与删除
消息从进入 RabbitMQ 到最终消失,经历以下阶段:
1. **写入内存**:消息到达队列进程后,先存入进程内存(Erlang ETS 表或进程状态)。
2. **写入磁盘**(如果 persistent):当内存压力达到一定阈值,或消息被标记为 persistent 时,消息体被写入磁盘文件。
3. **投递消费者**:消息从内存中读取并发送给消费者。
4. **等待 ACK**:消息在未收到 ACK 前不会被删除。
5. **收到 ACK 后删除**:消息从内存和磁盘中移除。对于持久化消息,磁盘文件中的标记被清除,空间在文件 compaction 时回收。
### 7. Go 代码示例
```go
package main
import (
amqp "github.com/rabbitmq/amqp091-go"
)
func main() {
conn, _ := amqp.Dial("amqp://guest:guest@localhost:5672/")
defer conn.Close()
ch, _ := conn.Channel()
defer ch.Close()
// 声明持久化队列:durable = true, autoDelete = false, exclusive = false
ch.QueueDeclare("order_events", true, false, false, false, amqp.Table{
"x-queue-type": "quorum", // 使用仲裁队列,替代默认的经典队列
})
// 发布持久化消息:DeliveryMode = Persistent
ch.Publish("", "order_events", false, false, amqp.Publishing{
ContentType: "application/json",
DeliveryMode: amqp.Persistent, // 消息持久化
Body: []byte(`{"order_id":"12345","status":"created"}`),
})
}
```
上面的代码展示了两个关键点:一是 `QueueDeclare` 设置 `durable: true` 并指定 `x-queue-type: quorum` 使用仲裁队列;二是 `Publishing` 设置 `DeliveryMode: amqp.Persistent` 确保消息持久化。两者缺一不可。
## 关联笔记
- [[07-主流MQ对比/23-RabbitMQ|RabbitMQ]]
- [[04-存储引擎/8-MQ-存储引擎设计|MQ 存储引擎设计]]
- [[05-可靠性保障/12-MQ-消息确认与持久化|MQ 消息确认与持久化]]
- [[03-协议与标准/5-AMQP-协议|AMQP 协议]]