RabbitMQ 消息可靠投递:confirm 确认 + 消费重试 + 死信队列
导读:消息队列最怕丢消息。简历投递、站内信这些场景,丢一条用户就收不到反馈。我系统性地把 RabbitMQ 的可靠投递做了一遍:生产端 confirm 确认、消费端手动 ack + 重试、失败进死信队列。这篇记录完整的配置和踩坑。
先说丢消息的三个环节:生产者发送失败、MQ 自己挂了、消费者处理失败。三个环节都要兜底,缺一个都不算可靠。

生产端:publisher confirm 确认
RabbitMQ 的 confirm 机制:发送消息后,Broker 会异步回调确认这条消息是否成功落盘。配置:
yaml
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
publisher-confirm-type: correlated # 开启 confirm
publisher-returns: true # 消息不可路由时回调
template:
mandatory: true
publisher-confirm-type 有三个值:none(不开启)、simple(同步等待)、correlated(异步回调)。生产用 correlated,别用 simple 阻塞发消息的线程。
发送消息并处理确认:
java
@Slf4j
@Component
@RequiredArgsConstructor
public class MqProducer {
private final RabbitTemplate rabbitTemplate;
// 发送简历投递通知
public void sendResumeDelivered(ResumeDeliveredEvent event) {
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend(
"job.exchange", "resume.delivered", event, cd);
// 异步回调确认结果
cd.getFuture().whenComplete((ack, ex) -> {
if (ack != null && ack.isAck()) {
log.info("消息确认成功 id={}", cd.getId());
} else {
log.error("消息确认失败 id={}, ack={}, cause={}",
cd.getId(), ack, ex == null ? "unknown" : ex.getMessage());
// 落库待重发:把消息存到本地表,定时任务补偿
resendStore.save(cd.getId(), event);
}
});
}
}
关键点:confirm 回调是异步的 ,ack.isAck() 为 false 表示发送失败,这时消息不能直接扔掉,要落一张本地表,定时任务扫表重发。
还有一个细节:publisher-returns: true + mandatory: true 是处理"消息发到了 exchange 但路由不到 queue"的情况(比如 routing key 写错)。这种情况 confirm 是成功的(消息确实到了 broker),但永远进不了队列。需要在 RabbitTemplate 设置 ReturnsCallback:
java
rabbitTemplate.setReturnsCallback(returned -> {
log.error("消息路由失败: exchange={}, routingKey={}, body={}",
returned.getExchange(), returned.getRoutingKey(),
new String(returned.getMessage().getBody()));
});
消费端:手动 ack + 重试
消费者默认是自动 ack------消息一取出来就确认,不管处理成不成功。必须改成手动 ack:
yaml
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual # 手动确认
retry:
enabled: true # 消费重试
max-attempts: 3 # 最多重试 3 次
initial-interval: 1000 # 重试间隔 1s
default-requeue-rejected: false
default-requeue-rejected: false 很关键:重试 3 次还失败的消息,不进原队列无限重试,而是走死信。
消费者代码:
java
@Slf4j
@Component
@RequiredArgsConstructor
public class ResumeDeliveredConsumer {
private final ResumeService resumeService;
@RabbitListener(queues = "job.resume.delivered.queue")
public void onMessage(ResumeDeliveredEvent event, Channel channel, Message message) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
resumeService.handleDelivered(event);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.error("处理简历投递消息失败: {}", event, e);
// 抛异常,走 Spring 重试;重试耗尽后进死信
throw new AmqpRejectAndDontRequeueException(e);
}
}
}
注意 catch 里不要自己吞异常 。我之前犯过:try-catch 里打了日志就返回,结果消息自动 ack 了,业务没处理,消息丢了。正确做法:处理失败抛 AmqpRejectAndDontRequeueException,Spring 会按配置重试,重试 3 次失败后拒绝入队(不重新放回原队列),转死信。
死信队列
死信队列声明:
java
@Configuration
public class MqConfig {
// 业务队列,绑定死信交换机
@Bean
public Queue resumeDeliveredQueue() {
return QueueBuilder.durable("job.resume.delivered.queue")
.deadLetterExchange("job.exchange.dlx")
.deadLetterRoutingKey("resume.delivered.dead")
.build();
}
// 死信交换机 + 死信队列
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange("job.exchange.dlx");
}
@Bean
public Queue resumeDeliveredDeadQueue() {
return QueueBuilder.durable("job.resume.delivered.dead.queue").build();
}
@Bean
public Binding deadBinding() {
return BindingBuilder.bind(resumeDeliveredDeadQueue())
.to(dlxExchange()).with("resume.delivered.dead");
}
}
死信队列里的消息,就是重试 3 次还失败的,人工排查或者定时任务捞出来补偿。
踩坑记录
坑一:confirm 回调里直接抛异常
现象:ack.isAck() 为 false 时,我在回调里直接 throw,结果消息没落库,还是丢了。回调线程是 MQ 的线程池,抛异常没人接,业务补偿逻辑根本没执行。解决:回调里只做落库和日志,不抛异常。
坑二:ack 之后才写完数据库,进程崩了
现象:先 channel.basicAck 再写业务表,恰好进程崩溃,消息确认了但业务没落库。解决:先处理业务,成功后再 ack(上面代码的顺序)。ack 之后业务已经完成,才算真正可靠。
坑三:消费者异常重试 3 次全在同一秒
现象:重试配置了 initial-interval: 1000,但日志里 3 次重试间隔几乎为 0。排查发现 retry.enabled 配在 listener.simple 下,但项目里用的是容器工厂手动创建的 listener,配置没生效。解决:确认 spring.rabbitmq.listener.simple.retry 的配置层级正确,或者直接注入 RetryInterceptorBuilder 手动配置。
坑四:死信消息又回到原队列死循环
现象:死信队列消息一直堆积,同时原队列也有重复消息。定位:default-requeue-rejected 没配,重试失败的消息被重新放回原队列,无限循环。解决:配 default-requeue-rejected: false,失败进死信而不是原队列。
可直接复用
- 生产端:
publisher-confirm-type: correlated+ confirm 回调落库补偿 - 路由失败:
publisher-returns: true+mandatory: true+ ReturnsCallback - 消费端:手动 ack,业务成功才 ack,失败抛
AmqpRejectAndDontRequeueException - 重试 3 次失败进死信队列,
default-requeue-rejected: false - 死信队列消息做人工补偿或定时重放
消息可靠性本质是"发送确认 + 消费确认 + 兜底补偿"三件套,每一环都不能省。这套在求职招聘系统里扛住了线上流量,照着配就行。