--- 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):交换机类型、怎么保证消息不丢、死信队列、延迟消息实现。