RabbitMQ重試機制

秋夜雨巷發表於2024-04-15

RabbitMQ重試機制+死信佇列

RabbitMQ重試機制

RabbitMQ的訊息重試機制,就是訊息消費失敗後進行重試,重試機制的觸發條件是消費者顯式的丟擲異常,這個很類似@Transactional,如果沒有顯式地丟擲異常或者try catch起來沒有手動回滾,事務是不會回滾的。

if("ACK重試機制".equals(messageBody)){
    message.getMessageProperties().getHeaders().put("x-death", count+1);
    throw new RuntimeException("手動出發異常,測試重試機制");
}

還有一種情況就是訊息被拒絕後重新加入佇列,比如basic.reject和basic.nack,並且requeue = true,但是這個是重新進入到了訊息佇列然後重新被消費,並且也不會觸發我們重試機制的配置(如重試間隔、最大重試次數等等)。重試機制是預設開啟的,但是如果沒有重試機制相關的配置會導致訊息一直無間隔的重試,直到消費成功,所以要使用重試機制一定要有相關配置。

死信佇列

死信就是訊息在特定場景下的一種表現形式,這些場景包括:

  1. 訊息被拒絕(basic.reject / basic.nack),並且requeue = false
  2. 訊息的 TTL 過期時
  3. 訊息佇列達到最大長度
  4. 達到最大重試限制

訊息在這些場景中時,被稱為死信。

死信佇列就是用於儲存死信的訊息佇列,在死信佇列中,有且只有死信構成,不會存在其餘型別的訊息。死信佇列也是一個普通佇列,也可以被消費者消費,區別在於業務佇列需要繫結在死信佇列上,才能正常地把死信傳送到死信佇列上。

業務佇列繫結死信佇列

    @Bean
    public Queue directQueue() {
        /**
         * 繫結死信交換機及路由key
         */
        Map<String, Object> args = new HashMap<>();
        // x-dead-letter-exchange:這裡宣告當前業務佇列繫結的死信交換機
        //訊息被拒絕、訊息過期,或者佇列達到其最大長度。訊息會變成死信
        args.put("x-dead-letter-exchange", DEAD_TCP_DATA_DIRECT_EXCHANGE);
        // x-dead-letter-routing-key:這裡宣告當前業務佇列的死信路由 key
        args.put("x-dead-letter-routing-key", DEAD_TCP_DATA_DIRECT_ROUTING);
        return QueueBuilder.durable(DIRECT_QUEUE).withArguments(args).build();
    }

自動ACK + RabbitMQ重試機制

appliction.properties

# 訊息重試機制: 自動ACK+MQ訊息重試
spring.rabbitmq.listener.simple.acknowledge-mode=auto
spring.rabbitmq.listener.simple.retry.enabled=true
spring.rabbitmq.listener.simple.retry.max-attempts=5
spring.rabbitmq.listener.simple.retry.initial-interval=5000

消費者

   @RabbitListener(queues = RabbitMqConfig.USER_ADD_QUEUE, concurrency = "3")
    public void userAddReceiver(String data, Message message, Channel channel) throws Exception {
        UserVo vo = OBJECT_MAPPER.readValue(data, UserVo.class);
        boolean success = messageHandle(vo);
        // 透過業務控制是否消費成功,消費失敗則丟擲異常觸發重試
        if (!success) {
            log.error("消費失敗");
            throw new Exception("訊息消費失敗");
        }
    }

一定要開啟自動ACK,才會在到達最大重試上限後傳送到死信佇列,而且在重試過程中會獨佔當前執行緒,如果是單執行緒的消費者會導致其他訊息阻塞,直至重試完成,所以可以使用@RabbitListener上的concurrency屬性來控制併發數量。
自動ACK後不需要

手動ACK + 手動重試機制

appliction.properties

# 手動ACK
spring.rabbitmq.listener.simple.acknowledge-mode=manual

手動ACK配置了重試機制,在丟擲異常的時候仍會觸發重試,但是達到重試上限之後,會永遠處於Unacked狀態,不會進入到死信佇列,必須要手動拒絕才可以進入死信佇列,所以說這裡不用配置重試機制而是採用手動重試的方式

消費者

    @RabbitHandler
    @RabbitListener(queues = DirectExchangeConfig.DIRECT_QUEUE2,concurrency = "3")
    public void process3(Message message, Channel channel) throws InterruptedException, IOException {
        // 重試次數
        int retryCount = 0;
        boolean success = false;
        // 消費失敗並且重試次數<=重試上限次數
        while (!success && retryCount < MAX_RETRIES) {
            retryCount++;
            // 具體業務邏輯
            String messageBody = new String(message.getBody(), "UTF-8");
            success = !messageBody.equals("ACK重試機制");  //如果訊息體等於ACK重試機制
            // 如果失敗則重試
            if (!success) {
                String errorTip = "第" + retryCount + "次消費失敗" +
                        ((retryCount < 3) ? "," + RETRY_INTERVAL + "s後重試" : ",進入死信佇列");
                log.error(errorTip);
                Thread.sleep(RETRY_INTERVAL * 1000);
            }
        }
        if (success) {
            // 消費成功,確認
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            log.info("消費成功");
        } else {
            // 重試多次之後仍失敗,進入死信佇列
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
            log.info("消費失敗");
        }
    }

使用spring-retry

pom.xml

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

編寫 RabbitMQ 的訊息消費者,同時在方法上新增 @Retryable 註解來指定重試策略。

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;

@Component
public class RabbitMQConsumer {

    @RabbitListener(queues = "your-queue-name")
    @Retryable(value = {Exception.class}, maxAttempts = 5, backoff = @Backoff(delay = 5000))
    public void handleMessage(Message message, Channel channel)  throws Exception{
        try {
            String messageBody = new String(message.getBody(), "UTF-8");
            int count = (int) message.getMessageProperties().getHeaders().getOrDefault("x-death", 1);
            log.info("{} DirectReceiver消費者收到訊息({}): {} ",Thread.currentThread(),count , messageBody);
            // 傳送第幾次
            if (count == 3){
                // 傳送確認
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
                return;
            }
        // 訊息處理邏輯
        // 如果發生異常,重試策略會在間隔5秒後再次嘗試執行,最多重試5次
    }
}

啟用重試機制:在 Spring Boot 的啟動類上新增 @EnableRetry 註解以啟用 Spring Retry。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;

@SpringBootApplication
@EnableRetry
public class YourApplication {

    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }
}

相關文章