Rabbit MQ實現重試和時間間隔機制 rabbitmq延遲發送消息

利用死信機制來實現重試和時間間隔機制,可以控制單獨的隊列重試次數和時間。
總體的思路就是,將消費失敗的消息發送到一個有過期時間的隊列中,該隊列沒有消費者,並且配置有死信隊列,那到了超時時間後,RabbitMQ會自動將該消息推送至死信隊列,監聽死信隊列的消費者,獲取消息後消費,從而實現控制時間間隔發送。而重試次數,可以通過在消息頭中設置增加一個參數來實現。
下面是我的實現方法:

首先需要聲明一個交換機和四個隊列,將四個隊列都綁定到交換機上。

String E_NOTIFY_CALLBACK = "E.NOTIFY_CALLBACK"; // 交換機
String Q_NOTIFY_CALLBACK_NORMAL = "Q.NOTIFY_CALLBACK@NORMAL"; // 正常隊列
String Q_NOTIFY_CALLBACK_READY = "Q.NOTIFY_CALLBACK@REDAY"; // 預備隊列(預備重試)
String Q_NOTIFY_CALLBACK_RETRY = "Q.NOTIFY_CALLBACK@RETRY"; // 重試隊列
String Q_NOTIFY_CALLBACK_DEAD = "Q.NOTIFY_CALLBACK@DEAD"; // 死信隊列

其中READY的隊列,比較特殊,也就是RETRY的死信隊列,需要設置消息的超時時間。
隊列和交換機可以聲明在RabbitConfig中,或者直接在監聽的方法上用註解形式聲明。

// 聲明交換機
@Bean
public DirectExchange notifyCallbackExchange() {
    return new DirectExchange(MQConfig.E_NOTIFY_CALLBACK);
}
// 聲明預備隊列
@Bean
public Queue notifyCallbackReadyQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-message-ttl", 10000); // 隊列消息超時時間
    args.put("x-dead-letter-exchange", MQConfig.E_NOTIFY_CALLBACK); // 死信交換機
    args.put("x-dead-letter-routing-key", "retry"); // 死信路由
    return new Queue(MQConfig.Q_NOTIFY_CALLBACK_READY, true, false, false, args);
}
// 綁定預備隊列到交換機,路由Key爲ready
@Bean
public Binding bindNotifyCallbackReadyQueue() {
    return BindingBuilder.bind(notifyCallbackReadyQueue()).to(notifyCallbackExchange()).with("ready");
}
// 聲明死信隊列
@Bean
public Queue notifyCallbackDeadQueue() {
    return new Queue(MQConfig.Q_NOTIFY_CALLBACK_DEAD);
}
// 綁定死信隊列到交換機,路由Key爲dead
@Bean
public Binding bindNotifyCallbackDeadQueue() {
    return BindingBuilder.bind(notifyCallbackDeadQueue()).to(notifyCallbackExchange()).with("dead");
}

其餘的隊列聲明:

@RabbitListener(bindings = @QueueBinding(
            value = @Queue(MQConfig.Q_NOTIFY_CALLBACK_NORMAL),
            exchange = @Exchange(value = MQConfig.E_NOTIFY_CALLBACK),
            key = "normal"))
@RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = MQConfig.Q_NOTIFY_CALLBACK_RETRY),
            exchange = @Exchange(value = MQConfig.E_NOTIFY_CALLBACK),
            key = "retry"))

NORMAL隊列

首先NORMAL隊列會獲取到一條消息,執行成功則結束邏輯,執行失敗則推送消息至READY隊列,等待重發。

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(MQConfig.Q_NOTIFY_CALLBACK_NORMAL),
        exchange = @Exchange(value = MQConfig.E_NOTIFY_CALLBACK),
        key = "normal"))
public void normal(NotifyCallbackMsg msg, Message message, Channel channel) {

    log.info("首次收到通知消息:[{}]", msg);
    try {
        // 業務邏輯

        // 直接模擬失敗,發送到預備重試隊列
        if (msg.getRetryTimes() > 0) {
            channel.basicPublish(MQConfig.E_NOTIFY_CALLBACK, "ready", basicProperties(message, 0), JSONObject.toJSONString(msg).getBytes());
        }
    } catch (IOException e) {
        try {
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        } catch (IOException ex) {
            log.error("MQ手動確認消息失敗!");
        }
    }
    try { // 手動確認消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (IOException e) {
        log.error("MQ手動確認消息失敗!");
    }
}

請求頭放入重試次數

private AMQP.BasicProperties basicProperties(Message message, int retryTimes) {
    Map<String, Object> headers = message.getMessageProperties().getHeaders();
    headers.put("retry-times", retryTimes);
    return new AMQP.BasicProperties().builder()
            .deliveryMode(2) // 傳送方式
            .contentEncoding("UTF-8") // 編碼方式
            .contentType("application/json")
            .headers(headers) //自定義屬性
            .build();
}

READY隊列

READY因爲全局配置了過期時間,args.put(“x-message-ttl”, 10000),因爲READY沒有消費者,超過10秒的消息會被自動投放到READY的死信隊列,即RETRY。

RETRY隊列

正式執行重發邏輯的隊列。

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = MQConfig.Q_NOTIFY_CALLBACK_RETRY),
        exchange = @Exchange(value = MQConfig.E_NOTIFY_CALLBACK),
        key = "retry"))
public void retry(NotifyCallbackMsg msg, Message message, Channel channel) {
    log.info("嘗試重新發送消息:[{}]", msg);
    try {
        int retryTimes = (int) message.getMessageProperties().getHeaders().get("retry-times");
        log.info("執行第[{}]次重發...", retryTimes++);
        // 業務邏輯
        // 直接模擬失敗
        if (retryTimes > msg.getRetryTimes()) {
            log.error("重試次數滿,進入死信隊列"); // 觸發告警
            channel.basicPublish(MQConfig.E_NOTIFY_CALLBACK, "dead", MessageProperties.PERSISTENT_BASIC, JSONObject.toJSONString(msg).getBytes());
        } else {
            channel.basicPublish(MQConfig.E_NOTIFY_CALLBACK, "ready", basicProperties(message, retryTimes), JSONObject.toJSONString(msg).getBytes());
        }
    } catch (IOException e) {
        try {
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        } catch (IOException ex) {
            log.error("MQ手動確認消息失敗!");
        }
    }
    try {
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    } catch (IOException e) {
        log.error("MQ手動確認消息失敗!");
    }
}

DEAD隊列

經過多次重發仍然失敗的任務,最終會被投放到這個隊列,用於排查問題,或者可以根據業務邏輯再實現消息的重啓。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章