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 是两个常被忽略的静默丢失点。
- 可靠投递只到"至少一次",最终正确性仍要靠消费端幂等。