RabbitMQ 如何保证消息不丢失?

#中间件#面试八股

RabbitMQ 如何保证消息不丢失?

结论:消息从生产到消费要经过七个站,每一站都有独立的丢失风险:发送未确认、路由不到队列、队列不持久、消息不持久、副本未同步、队列溢出或 TTL 过期、消费未确认。可靠投递就是把七站的开关逐个打开,并接受"至少一次"带来的重复,用幂等收尾。

一、七个丢失点

Producer ──①未开 confirm──> Exchange ──②路由不到队列被丢弃──> Queue
                                                              │
                    ③队列非 durable(重启消失)                  │
                    ④消息非 persistent(重启消失)                │
                    ⑤无副本,节点宕机                             │
                    ⑥max-length 溢出 / TTL 过期                  │
                                                              ▼
Consumer <──⑦autoAck 或处理失败仍 ack── 业务
站点丢失场景对应措施
① 生产发送网络中断、Broker 未收到开启发布确认(Publisher Confirm),失败重发
② 路由routing key 无匹配队列mandatory=true + basic.return 回调,或声明备用交换机(Alternate Exchange)
③ 队列队列未声明 durable,重启丢失queueDeclare(..., durable=true, ...)
④ 消息消息未设持久化MessageProperties.PERSISTENT_TEXT_PLAIN 或 deliveryMode=2
⑤ 副本单节点队列,节点故障仲裁队列(Quorum Queue)或镜像队列(已移除)
⑥ 容量队列超长丢弃队头、消息过期设置 x-max-length 时慎用 drop-head,合理设置 TTL 并监控
⑦ 消费自动 ack、处理失败仍 ack、prefetch 过大手动 ack,成功才确认,失败进重试或死信

二、生产端:confirm + return

channel.confirmSelect();
channel.addConfirmListener((seq, multiple) -> { /* 已确认,可删除本地记录 */ },
                           (seq, multiple) -> { /* 未确认,重发或告警 */ });
channel.addReturnListener(rm -> {
    // 路由失败:没有队列匹配 routing key
    log.error("unroutable: {} {}", rm.getRoutingKey(), rm.getReplyText());
});
// mandatory=true 才会触发 return 回调
channel.basicPublish("order.ex", "order.pay.created", true,
        MessageProperties.PERSISTENT_TEXT_PLAIN, body);
channel.waitForConfirmsOrDie(5000);

confirm 是异步的,waitForConfirmsOrDie 只是等一批结果;高吞吐场景应使用 ConfirmListener + 本地待确认表,避免同步等待拖慢发送。

三、Broker 端:持久化 + 副本

Map<String, Object> args = Map.of(
        "x-queue-type", "quorum",          // Raft 三副本
        "x-max-length", 1_000_000,         // 上限,防止无限堆积
        "x-overflow", "reject-publish");   // 溢出拒绝新消息,而不是丢队头
channel.exchangeDeclare("order.ex", BuiltinExchangeType.TOPIC, true);
channel.queueDeclare("order.pay.q", true, false, false, args);
channel.queueBind("order.pay.q", "order.ex", "order.pay.*");

// 声明备用交换机,路由失败的消息转到这里而不是被丢弃
channel.exchangeDeclare("order.ex.alt", BuiltinExchangeType.FANOUT, true);
Map<String, Object> exArgs = Map.of("alternate-exchange", "order.ex.alt");
channel.exchangeDeclare("order.ex", BuiltinExchangeType.TOPIC, true, false, exArgs);

x-overflow=reject-publish 配合生产端 confirm 很重要:队列满时 Broker 会 nack,生产者能感知并重试;默认的 drop-head 会静默丢最老的消息,正好丢的是尚未消费的那批。

四、消费端:手动 ack

channel.basicQos(50);
channel.basicConsume("order.pay.q", false, (tag, msg) -> {
    try {
        handler.handle(msg);                 // 同步处理完
        channel.basicAck(tag, false);        // 成功才确认
    } catch (Exception e) {
        channel.basicNack(tag, false, false); // 不重回队列,进死信
    }
});

五、常见追问

  • 只把队列设为 durable,消息会丢吗? 会。队列持久化只保证队列定义存在,消息本身默认非持久化,重启即丢。
  • confirm 和事务(tx)怎么选? confirm 是异步批量确认,性能好;tx 是同步提交,吞吐低一个数量级,生产环境基本只用 confirm。
  • 仲裁队列下 confirm 代表什么? 代表消息已被多数派副本接受,比经典队列的 confirm 更强。
  • 消息发出去了但消费者一直没收到,怎么排查? 先看 rabbitmqctl list_queues 的 messages 是否堆积,再看绑定是否匹配、basic.return 是否被忽略、vhost 是否写错。

小结

  • 不丢是七站联动的结果,任何一站使用默认值都可能丢。
  • 生产端 confirm + mandatory,Broker 端持久化 + 仲裁队列,消费端手动 ack,是三段主线。
  • 队列溢出策略与 TTL 是两个常被忽略的静默丢失点。
  • 可靠投递只到"至少一次",最终正确性仍要靠消费端幂等。
笔记加载中…