Skip to content

RabbitMQ 消费失败重试设计(重试次数、死信、延迟重试)

这篇解决一个最常见的线上问题:消费失败了,怎么重试才不把系统拖死?

先说结论

可靠的重试策略必须同时满足三点:

  1. 有上限:避免无限重试
  2. 有间隔:避免瞬间把下游打挂
  3. 可观测:失败能追踪,死信能处理

一、重试失败的 3 种典型场景

  1. 业务异常:第三方接口超时
  2. 系统异常:数据库连接池耗尽
  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 次直接死信。
这样既能覆盖短暂波动,也能避免拖垮系统。


五、死信队列一定要处理

死信不是“失败消息仓库”,它是你排障的入口。

至少要做:

  1. 告警通知(钉钉 / 邮件)
  2. 标记消息状态(数据库 or ES)
  3. 手动/自动补偿

六、完整消费者示例

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);
    }
}

最后总结

一个靠谱的重试体系,必须做到:

  1. 明确可重试 vs 不可重试
  2. 有重试次数上限
  3. 延迟间隔逐级拉长
  4. 死信有兜底处理

做到这四点,线上重试就不会再变成“雪崩放大器”。