RocketMQ 消费失败怎么办?一文搞懂重试机制 + 死信队列实战

6 阅读3分钟

在微服务架构中,消息队列是解耦、削峰、异步的核心组件。但现实很骨感——消费者可能因网络抖动、数据库超时、业务异常等原因消费失败。如果处理不当,轻则数据丢失,重则系统雪崩。

Apache RocketMQ 提供了完善的失败重试 + 死信队列(DLQ)机制,帮助我们优雅应对消费异常。本文将从原理到实战,手把手教你:

  • 消费失败后如何自动重试?
  • 什么条件下消息会进入死信队列?
  • 如何编写代码消费死信消息并人工干预?

一、消费失败 ≠ 消息丢失:RocketMQ 的重试机制

当消费者处理消息抛出异常或返回 ConsumeConcurrentlyStatus.RECONSUME_LATER 时,RocketMQ 不会丢弃消息,而是将其放入重试队列(Retry Queue) ,延迟后重新投递。

✅ 重试规则(集群模式下)

  • 默认最多重试 16 次

  • 采用指数退避策略,延迟时间依次为:

    1s → 5s → 10s → 30s → 1m → 2m → ... → 2h
    
  • 重试消息存储在特殊 Topic:%RETRY%<消费者组名>

📌 广播模式(BROADCASTING)不支持重试!

🔧 自定义最大重试次数

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order-service-group");
consumer.setMaxReconsumeTimes(3); // 超过3次失败就进死信队列

二、死信队列(DLQ):最后的安全网

当消息重试次数达到上限仍未成功,RocketMQ 会将其投递到死信队列(Dead Letter Queue) ,避免无限循环重试。

📥 进入 DLQ 的条件

  • 消费失败 + 重试次数 ≥ maxReconsumeTimes
  • 消息未过期(默认保留 3 天)

🏷️ DLQ 命名规则

%DLQ%<消费者组名>
// 例如:%DLQ%order-service-group

⚠️ 无需手动创建!第一条消息进入时自动创建。


三、实战:模拟消费失败 → 进入 DLQ → 人工处理

步骤 1:启动一个“故意失败”的消费者

// OrderConsumer.java
public class OrderConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order-service-group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("ORDER_TOPIC", "*");
        consumer.setMaxReconsumeTimes(2); // 仅重试2次

        consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
            for (MessageExt msg : msgs) {
                System.out.println("收到消息: " + new String(msg.getBody()));
                // 模拟业务异常
                throw new RuntimeException("数据库连接失败!");
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();
        System.out.println("订单消费者启动,故意失败...");
    }
}

运行后,你会看到日志:

收到消息: {"orderId": "1001"}
收到消息: {"orderId": "1001"}  // 5秒后
收到消息: {"orderId": "1001"}  // 10秒后
// 第3次失败 → 消息进入 %DLQ%order-service-group

步骤 2:编写死信消费者,人工处理

// DeadLetterConsumer.java
public class DeadLetterConsumer {
    public static void main(String[] args) throws MQClientException {
        // ⚠️ 必须使用相同的消费者组!
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order-service-group");
        consumer.setNamesrvAddr("localhost:9876");

        // 订阅死信队列
        String dlqTopic = "%DLQ%order-service-group";
        consumer.subscribe(dlqTopic, "*");

        consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
            for (MessageExt msg : msgs) {
                try {
                    System.out.println("【死信消息】MsgId: " + msg.getMsgId());
                    System.out.println("原始 Topic: " + msg.getProperty(MessageConst.PROPERTY_REAL_TOPIC));
                    System.out.println("消息内容: " + new String(msg.getBody()));

                    // TODO: 人工处理逻辑
                    // 1. 记录到数据库(含错误原因)
                    // 2. 发送企业微信/钉钉告警
                    // 3. 修复后重新发送到原 Topic(谨慎操作!)

                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

                } catch (Exception e) {
                    // DLQ 本身不再重试!务必返回 SUCCESS 避免堆积
                    log.error("处理死信失败,MsgId={}", msg.getMsgId(), e);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();
        System.out.println("死信消费者启动成功");
    }
}

💡 关键点:

  • Consumer Group 必须与原消费者一致
  • 永远不要在 DLQ 消费者中返回 RECONSUME_LATER(否则会无限堆积)

四、生产环境最佳实践

✅ 1. 监控 + 告警

  • 使用 mqadmin 或 RocketMQ Dashboard 监控 DLQ 消息量
  • 设置告警:DLQ 消息数 > 0

✅ 2. 提供人工干预入口

开发一个管理后台,支持:

  • 查看死信消息详情
  • 一键重发(发送到原 Topic)
  • 标记为“已忽略”

✅ 3. 日志记录完整上下文

log.warn("消息进入死信队列 | MsgId={} | OriginalTopic={} | Body={}",
    msg.getMsgId(),
    msg.getProperty(MessageConst.PROPERTY_REAL_TOPIC),
    new String(msg.getBody())
);

✅ 4. 避免“重试地狱”

对于确定性业务错误(如 JSON 格式错误),应直接记录日志并返回 CONSUME_SUCCESS不要触发重试


五、总结

场景RocketMQ 行为
消费失败进入重试队列 %RETRY%group,延迟重试
重试超限进入死信队列 %DLQ%group
DLQ 消费需手动编写消费者,无自动重试
消息保留默认 3 天(可配置)

死信队列不是垃圾桶,而是“待救援区”
合理利用它,才能真正做到 消息不丢、故障可溯、系统健壮