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,但是這個是重新進入到了訊息佇列然後重新被消費,並且也不會觸發我們重試機制的配置(如重試間隔、最大重試次數等等)。重試機制是預設開啟的,但是如果沒有重試機制相關的配置會導致訊息一直無間隔的重試,直到消費成功,所以要使用重試機制一定要有相關配置。
死信佇列
死信就是訊息在特定場景下的一種表現形式,這些場景包括:
- 訊息被拒絕(basic.reject / basic.nack),並且requeue = false
- 訊息的 TTL 過期時
- 訊息佇列達到最大長度
- 達到最大重試限制
訊息在這些場景中時,被稱為死信。
死信佇列就是用於儲存死信的訊息佇列,在死信佇列中,有且只有死信構成,不會存在其餘型別的訊息。死信佇列也是一個普通佇列,也可以被消費者消費,區別在於業務佇列需要繫結在死信佇列上,才能正常地把死信傳送到死信佇列上。
業務佇列繫結死信佇列
@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);
}
}