利用死信機制來實現重試和時間間隔機制,可以控制單獨的隊列重試次數和時間。
總體的思路就是,將消費失敗的消息發送到一個有過期時間的隊列中,該隊列沒有消費者,並且配置有死信隊列,那到了超時時間後,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隊列
經過多次重發仍然失敗的任務,最終會被投放到這個隊列,用於排查問題,或者可以根據業務邏輯再實現消息的重啓。