1. 为什么要消息队列?
很多业务不是一个接口干到底,下单之后还要发短信、推仓储、写搜索索引------如果全同步串行,一个环节慢或挂了,整条链路都受影响。
消息队列解决三类问题:
- 异步:主流程只负责发消息,下游慢慢处理,用户不用干等
- 解耦:订单服务不用直接依赖短信、仓储,各改各的,接口不变
- 削峰:秒杀瞬间流量先堆在队列里,消费者按自己的处理能力慢慢消费
代价是系统变复杂:重复消费、顺序、延迟、运维都要额外考虑。MQ 换的是可用性和扩展性,不是加了就更快。
2. 什么场景用 RabbitMQ?
适合:服务之间发业务事件、任务分发、需要灵活路由(不同消息进不同队列)、吞吐量在万级/秒量级。
不太适合:调用方必须立刻拿到下游结果才能继续;要回放全量历史日志做流式计算(更适合 Kafka);团队没人运维却上了要精细调参的集群。
和 Redis 队列比:Redis List / Stream 适合量小、能丢几条;RabbitMQ 有确认、持久化、死信,适合业务消息要可靠投递的场景。
3. 消息怎么从生产者到消费者?
RabbitMQ 走 AMQP 模型,路径是:
text
生产者 -> Exchange(交换机)-> Queue(队列)-> 消费者
Exchange 不存消息 ,只负责路由------看 routing key 和 binding 规则,决定消息进哪个 Queue。Queue 才存消息,等消费者来取。
很多人误以为生产者直接发 Queue,其实中间必须经过 Exchange(除非用默认 Exchange,routing key 必须等于 queue 名,那是 direct 的特例)。
Binding 是 Queue 和 Exchange 之间的绑定关系,声明什么样的 routing key 能进我这个队列。
4. Exchange 四种类型
| 类型 | 怎么路由 | 典型用法 |
|---|---|---|
| direct | routing key 和 binding key 完全相等 | 按业务类型分发:支付走支付队列,取消走取消队列 |
| fanout | 不看 routing key,广播到所有绑定的队列 | 配置刷新、缓存失效,通知所有节点 |
| topic | routing key 按 . 分段,* 匹配一个词,# 匹配零个或多个 |
日志分级:order.* 收所有订单日志,order.payment.# 只收支付相关 |
| headers | 按消息头匹配 | 几乎不用,知道有就行 |
日常 direct 和 topic 最多。fanout 做广播。
5. 怎么保证消息不丢?
可靠性要保三段,缺一段都不行:
① 生产者 → Broker
发完不能默认成功。开 Publisher Confirm,Broker 收到并路由后会回 ack;没有队列能接收时回 nack 或 return(需开 mandatory)。别用事务模式,性能差。
② Broker 存盘
Broker 重启消息还在,需要三个开关同时开:
- Exchange 声明
durable = true - Queue 声明
durable = true - 消息
delivery mode = 2(persistent)
持久化是写磁盘,不是魔法,仍受刷盘策略影响,且会降低吞吐。
③ Broker → 消费者
生产环境用 manual ack ,业务处理成功再确认。auto ack 是一推给消费者就确认了,处理到一半进程挂了,消息就丢了。
- Ack:处理成功,告诉 Broker 这条可以删了
- Nack (Negative Acknowledgment):处理失败,告诉 Broker 这条没消费成功
requeue: true:放回队列,稍后重试requeue: false:不再重投,配合死信交换机进 DLQ
6. Prefetch 是什么?
basicQos(prefetchCount: n) 限制单个 Channel 上还没 ack 的消息最多几条。
不是把总数平均分给所有消费者。每个消费者各自有一个 Channel,各自有自己的 prefetch 上限。Broker 推消息时,某消费者 unacked 条数已达上限,就不再给它推,转给其他还没满的消费者。
prefetch = 1 是常见配置:每个消费者同一时间最多拿 1 条,ack 后才拿下一条。多消费者共抢一个队列时,快慢消费者之间分配较均匀,慢消费者也不会一口气占很多消息不 ack。代价是吞吐略低(往返次数多);消息小、处理快时可调到 10~50。
不设 prefetch 时,Broker 可能一次性推几百条给某个消费者,慢消费者内存压力大,其他消费者反而空闲。prefetch 是 per-channel 的,每个消费线程用自己的 Channel,各自配置。
7. 重复消费与投递语义
第 5 节讲的是消息不丢。这一节讲另一个问题:同一条消息,消费者可能收到不止一次。
| 语义 | 含义 | RabbitMQ 能不能做到 |
|---|---|---|
| 最多一次 | 可能丢,但不会重复 | auto ack 就是这样,不推荐 |
| 至少一次 | 不会丢,但可能重复 | Confirm + 持久化 + manual ack,生产常见 |
| 恰好一次 | 不丢也不重复 | Broker 端到端做不到,靠业务幂等兜底 |
RabbitMQ 配好了可靠性,实际就是至少一次:消息一定能送到,但同一条可能被消费两次甚至更多。MQ 不负责去重,消费端要自己扛。
重复常见来源有三类。生产者重试 :发完没收到 Confirm,以为失败又发一遍,队列里两条内容一样的消息。消费者 ack 超时或忘了 ack :业务已跑完(DB 已扣库存),但 Ack 没发出去或处理超时,Broker 认为还没消费成功,会重新投递。网络抖动:Ack 包丢了,Broker 没收到确认,同样重投。本质都是 Broker 不知道处理完了,所以再发一次。
幂等 指同一个操作执行多次,结果和执行一次一样。支付消息来两次,库存只能扣一次。消费端不能假设消息只来一次,重复只能在消费端挡:用 orderId + eventType 做幂等表,插过就跳过;或对业务键加数据库唯一约束;或用状态机,订单已是 Paid 就不再扣款。
8. 死信队列(DLX)
正常队列里的消息,消费者取出来处理、Ack 掉。有些消息在正常流程里走不下去了------消费不了、过期了、队列满了------既不能一直占着业务队列重试,也不能直接丢掉,这类叫死信(Dead Letter)。
**死信队列(DLQ)**专门收这些消息,相当于业务队列旁边的隔离区。配置上:业务队列声明 x-dead-letter-exchange 指向死信交换机(DLX),DLX 再绑定 DLQ;消息一旦变成死信,Broker 自动转过去。
变成死信的情况:Nack(requeue: false) 不再重试;消息或队列 TTL 过期;队列超过 max-length 被挤出。
DLQ 三个常见用途。隔离毒消息 :每次消费都抛异常的消息,若一直 Nack(requeue: true) 会无限循环;本地重试 N 次仍失败就 Nack(requeue: false) 进 DLQ,业务队列恢复。人工排查补偿 :查看 DLQ 内容、修数据后重新发回或脚本补偿。延迟投递:消息设 TTL,过期变死信进 DLQ,消费者监听 DLQ 相当于延迟到了才处理(见第 10 节)。
DLQ 和业务队列分开建,用于告警、排查、补偿,不混在主流程里。
9. 消息顺序能保证吗?
单队列 + 单消费者:基本有序。
同一队列多个消费者:并发处理,顺序没法保证。
要大体有序:按业务键 hash 到固定队列(比如 orderId % 4 进 4 个队列之一),每个队列只开一个消费者;或者业务层容忍乱序,用版本号做合并。别说 MQ 保证顺序。
10. 延迟消息
订单 30 分钟未支付自动关单,需要延迟投递。常见做法:
- TTL + DLX:消息设 TTL,过期后进死信队列,相当于延迟到了才消费
- delayed-message-exchange 插件:最直接
- 业务层延迟表 + 定时扫描:不依赖 MQ 特性
TTL + DLX 要注意:同队列里不同 TTL 的消息,队头没过期会堵住后面的,叫队头阻塞。
11. 和 Kafka 怎么选?
| RabbitMQ | Kafka | |
|---|---|---|
| 定位 | 消息代理,智能路由 | 分布式日志,高吞吐流 |
| 消费后 | 通常 ack 后从队列删除 | 消息保留,可回溯重放 |
| 吞吐量 | 万级/秒 | 百万级/秒 |
| 典型场景 | 任务分发、业务事件、服务解耦 | 日志采集、流计算、事件溯源 |
订单支付通知仓储、发短信------RabbitMQ 合适。用户行为日志进数仓、Flink 实时聚合------Kafka 合适。不是谁更好,是问题类型不同。
12. 本地事务和发 MQ 怎么一致?
用户下单,同一个操作里既要写数据库(插订单),又要发 MQ 通知仓储备货。至少不能出现:库里有订单但仓储没收到消息,或仓储收到消息但订单没建成。
数据库和 RabbitMQ 是两个独立系统,先写 DB 再发 MQ 是两步,任何一步都可能单独失败。DB 成功、发 MQ 前程序崩溃------仓储永远不知道。MQ 发出去了、DB 事务回滚------仓储收到备货通知,订单不存在。在 DB 事务里直接 BasicPublish 也解决不了,MQ 提交不归 DB 管,DB 回滚带不走已发出的消息。
Outbox 的做法:发 MQ 这件事先记到 DB 里,跟业务数据进同一个本地事务。
text
1. 开启 DB 事务,插入订单表,插入 outbox 表(payload、status=pending)
2. 提交事务 ------ 订单和 outbox 要么都在,要么都不在
3. 后台 Worker 扫 outbox,发到 RabbitMQ,成功则标记 sent
Worker 发完还没标记就挂了,可能重复发,靠消费者幂等(第 7 节)。还没发就挂了,outbox 仍是 pending,下次重试。最终至少会发出去一次。
消费端同理:DB 事务处理业务并提交,成功再 Ack,失败 Nack;Ack 前做好幂等。DB 和 MQ 的一致性靠 Outbox + 消费者幂等做最终一致。
13. 消息堆积怎么排查?
先看队列深度,分 ready (等着被消费)和 unacked(已推给消费者还没 ack):
- unacked 高:消费者慢、卡住了、忘了 ack、prefetch 设太大
- ready 高、消费者少:扩容消费者,或优化单条处理逻辑
- 没有消费者:订阅丢了、连接断了、vhost 权限不对
- 单条处理太慢:瓶颈在下游 DB 或 API,加消费者没用
先分清是生产太快还是消费太慢,再动刀。内存/磁盘告警时 RabbitMQ 会流控阻塞生产者,常见原因也是消费者停了或消息不 ack 堆着。
14. 高可用与集群
RabbitMQ 的高可用分两层:多台 Broker 组集群 ,以及队列多副本(消息不随单节点挂掉而丢)。
集群是多台节点连成一组,共享 Exchange、Queue、Binding 等元数据定义。客户端连任意节点都能发收消息,节点之间同步路由、互相转发。三台组了集群,不等于三条消息分别存三台------集群同步的是定义,普通队列里的消息内容默认只放在声明这个队列的那台节点上。
集群有 A、B、C 三台,应用在 A 上声明了 order.queue:
text
A 节点:存着 order.queue 里的 1000 条消息
B、C 节点:知道 order.queue 存在,可转发,但本地没有这 1000 条
连 B 发消息,B 转给 A 存;连 B 消费,B 从 A 取。A 挂了,这个普通队列暂时不可用。集群管的是组网、路由、接入:拓扑全集群一致,连接可分散到任意节点,Quorum Queue 等多副本方案也得以集群为前提。消息本身要不丢,还得靠下面的队列类型。
| 类型 | 说明 |
|---|---|
| 普通队列 | 数据在单个节点,节点挂了就完了 |
| 经典镜像队列 | 老方案,主从同步复制,有脑裂等问题,老项目可能还在用 |
| Quorum Queue | 3.8+ 推荐,Raft 共识多副本,主节点挂了自动切换 |
生产新部署优先 Quorum Queue,集群至少 3 节点,副本数通常 3。
和 Redis Cluster 不同:Redis 按 key 分片,每个 key 只在一台;RabbitMQ 集群是组网 + 队列选副本策略,一条队列可通过 Quorum 在多个节点有副本。
RabbitMQ 不是消息追踪系统,消息 ack 后从队列删除,没法按 ID 查历史;追踪靠业务层日志或 outbox 状态表。