RabbitMQ 消费失败重试设计(重试次数、死信、延迟重试)
这篇解决一个最常见的线上问题:消费失败了,怎么重试才不把系统拖死?
先说结论
可靠的重试策略必须同时满足三点:
- 有上限:避免无限重试
- 有间隔:避免瞬间把下游打挂
- 可观测:失败能追踪,死信能处理
一、重试失败的 3 种典型场景
- 业务异常:第三方接口超时
- 系统异常:数据库连接池耗尽
- 数据异常:消息内容非法
这里最重要的是区分“可重试”和“不可重试”。
二、推荐的重试模型
核心思路:短暂问题走重试,逻辑错误走死信。
三、实现方式(两套常用方案)
方案 1:TTL + 死信队列(不依赖插件)
java
// 延迟队列配置(TTL + DLX)
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000); // 5s 后重试
args.put("x-dead-letter-exchange", "retry.exchange");
args.put("x-dead-letter-routing-key", "retry.key");
Queue retryQueue = new Queue("retry.queue", true, false, false, args);失败时把消息发到 retry 队列,5 秒后会自动转到真正的消费队列。
方案 2:延迟插件(更灵活)
java
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
DirectExchange delayExchange = new DirectExchange(
"delay.exchange", true, false, args
);
rabbitTemplate.convertAndSend("delay.exchange", "delay.key", message, m -> {
m.getMessageProperties().setHeader("x-delay", 10000);
return m;
});插件方式可以动态指定延迟时间,不需要多个 retry 队列。
四、重试次数怎么定
我的常用配置:
- 1 次:1s
- 2 次:5s
- 3 次:30s
- 4 次:2min
- 5 次:10min
超过 5 次直接死信。
这样既能覆盖短暂波动,也能避免拖垮系统。
五、死信队列一定要处理
死信不是“失败消息仓库”,它是你排障的入口。
至少要做:
- 告警通知(钉钉 / 邮件)
- 标记消息状态(数据库 or ES)
- 手动/自动补偿
六、完整消费者示例
java
@RabbitListener(queues = "order.queue")
public void onMessage(OrderMessage msg, Channel channel, Message message) throws IOException {
try {
process(msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (RetryableException e) {
// 可重试,送去延迟队列
sendToRetry(msg, 1);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 不可重试,送死信
sendToDeadLetter(msg, e.getMessage());
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}最后总结
一个靠谱的重试体系,必须做到:
- 明确可重试 vs 不可重试
- 有重试次数上限
- 延迟间隔逐级拉长
- 死信有兜底处理
做到这四点,线上重试就不会再变成“雪崩放大器”。