Files
zhuangang/topics/backend/mq/01-rabbitmq.md
T

103 lines
5.3 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.
---
id: backend.mq.rabbitmq
title: RabbitMQ:交换机、队列、确认机制、死信和延迟
type: lesson
domain: backend
stack: [mq]
tags: [RabbitMQ, AMQP, 交换机, ACK, 死信, 延迟队列]
level: basic
est_hours: 3
related: [backend.mq.reliability, backend.mq.scenario.delayed-order-cancel, backend.mq.interview]
week: [3, 14]
order: 1
---
# RabbitMQ:交换机、队列、确认机制、死信和延迟
## 解释说明
**模型**:生产者 → **交换机(Exchange)** →(按绑定规则路由)→ **队列(Queue)** → 消费者。生产者不直接发给队列,而是发给交换机并带一个 routing key。
| 交换机类型 | 路由规则 | 场景 |
| --- | --- | --- |
| direct | routing key 完全匹配 | 按类型分发任务 |
| topic | 通配符匹配,`*` 一个词、`#` 零个或多个词,如 `order.*.created` | 按业务域订阅事件 |
| fanout | 忽略 routing key,发给所有绑定的队列 | 广播 |
| headers | 按消息头匹配 | 少用 |
还有一个“默认交换机”:routing key 等于队列名时直接投递到该队列。
**可靠性三件套**
- **持久化**:交换机、队列声明为 durable,消息设置 `delivery_mode=2`(持久化),重启后不丢。
- **生产者确认(publisher confirm)**:broker 收到并处理好消息后回 ack,生产者才认为发送成功。
- **消费者手动确认(manual ack)**:处理完成后再 ack;处理失败 nack,可以选择重新入队或进入死信。消费者断开时未确认的消息会重新投递,所以消费要幂等。
**prefetch(QoS)**:每个消费者最多同时持有多少条未确认消息,防止一个消费者拿太多、其他消费者闲着。
**死信(Dead Letter)**:消息被拒绝且不重新入队、过期(TTL)、队列满时,会被转到队列配置的死信交换机。用途:失败消息集中处理、延迟消息。
**延迟消息**的两种做法:TTL + 死信(消息在没有消费者的等待队列中过期,转到处理队列);或者安装官方社区的延迟消息插件。注意:消息级 TTL 只在消息到达队头时检查,不同 TTL 的消息混在一个队列里会互相阻塞,所以**一个等待队列只放同一种延迟时间**。
**队列类型**:RabbitMQ 4.x 推荐需要高可用和数据安全的队列使用 **quorum 队列**(基于 Raft 复制),经典队列的镜像功能已移除。具体特性以官方文档为准:https://www.rabbitmq.com/docs/quorum-queues
## 案例
- 用户注册后发欢迎邮件、送新人券:注册服务发 `user.registered` 到 topic 交换机,邮件服务和营销服务各自建队列绑定,互不影响。
- 订单 30 分钟未支付自动取消:见[延迟取消场景](scenarios/delayed-order-cancel.md)。
## 代码示例
Python + aio-pika,生产者带确认、消费者手动确认,失败的消息进死信队列(示意代码,完整可运行示例见 [examples/delay-cancel](examples/delay-cancel/)):
```python
import asyncio
import json
import aio_pika
async def main() -> None:
conn = await aio_pika.connect_robust("amqp://guest:guest@localhost/")
async with conn:
ch = await conn.channel(publisher_confirms=True) # 默认就是开启的;publish 会等待 broker 确认
await ch.set_qos(prefetch_count=20)
events = await ch.declare_exchange("events", aio_pika.ExchangeType.TOPIC, durable=True)
dlx = await ch.declare_exchange("events.dlx", aio_pika.ExchangeType.FANOUT, durable=True)
dead = await ch.declare_queue("email.dead", durable=True)
await dead.bind(dlx)
q = await ch.declare_queue("email.welcome", durable=True,
arguments={"x-dead-letter-exchange": "events.dlx", "x-queue-type": "quorum"})
await q.bind(events, routing_key="user.registered")
await events.publish(
aio_pika.Message(json.dumps({"user_id": 1}).encode(), delivery_mode=aio_pika.DeliveryMode.PERSISTENT,
message_id="user-1-registered"),
routing_key="user.registered")
async with q.iterator() as it:
async for msg in it:
# requeue=False:处理失败不重新入队,直接进死信队列,避免无限重试
async with msg.process(requeue=False):
data = json.loads(msg.body)
print("发送欢迎邮件给用户", data["user_id"])
break
asyncio.run(main())
```
## 易错点
- **自动 ack**:消息一到就确认,消费者处理到一半崩溃,消息就丢了。
- **失败消息无限重新入队**:一条有问题的消息反复失败,堵住队列、刷爆日志。设置重试次数,超过进死信。
- **以为开启持久化就不会丢**:还需要生产者确认;单节点磁盘故障仍会丢,需要 quorum 队列。
- **一个等待队列放不同 TTL 的消息**:后面短 TTL 的消息被前面长 TTL 的挡住。
- **队列声明参数不一致**:同名队列用不同参数重复声明会报错(PRECONDITION_FAILED),要删除重建或换名。
- **prefetch 设成无限**:一个消费者囤积大量消息,内存暴涨、负载不均。
## 面试常问
见[消息队列面试题](interview.md):交换机类型、怎么保证消息不丢、死信队列、延迟消息实现。