什么是死信隊列
在RabbitMQ中一條消息出現下面三種情況就會成為死信:
- 消息被nack或者reject且requeue參數為false
- 消息因TTL過期
- 隊列超出長度限制
死信會被RabbitMQ特殊處理,如果配置了死信隊列則會進入到死信隊列中。如果沒有配置,該消息則會被丟棄。
如何配置死信隊列
配置死信隊列非常簡單,只需要完成下面幾步即可。下面是我們需要用到的相關配置:
名稱 | 值 | 備注 |
---|---|---|
業務Exchange | order.direct | direct類型Exchange |
業務Queue | order.queue | |
業務Routing Key | order | |
死信Exchange | dead.direct | direct類型Exchange |
死信Queue | dead.queue | |
死信Routing Key | dead |
正常業務配置
表格中前三行是我們正常的業務配置,與平常配置不一樣的地方在于Queue的配置,在Queue中我們在其Arguments中增加兩個配置:x-dead-letter-exchange和x-dead-letter-routing-key。其中x-dead-letter-exchange中的值代表了消息稱為死信之后將會被發送到哪個Exchange,而x-dead-letter-routing-key表示變成死信之后它的RoutingKey。所以我們增加的配置如下:
x-dead-letter-exchange = dead.direct
x-dead-letter-routing-key = dead
死信隊列相關配置
我們在上面正常的業務Queue中增加了x-dead-letter-exchange和x-dead-letter-routing-key的配置,接下來就是創建配置中對應的Exchange和Queue。這里的Exchange和Queue與我們平常使用的并沒有什么區別。
配置代碼如下
下面是定義Exchange和Queue相關的主要代碼:
/**
* 正常的業務Exchange和Queue
*/
channel.exchangeDeclare(Config.BUSINESS_EXCHANGE, BuiltinExchangeType.DIRECT, true);
Map<String, Object> arg = new HashMap<>();
arg.put("x-dead-letter-exchange", Config.DEAD_EXCHANGE);
arg.put("x-dead-letter-routing-key", Config.DEAD_ROUTING_KEY);
channel.queueDeclare(Config.BUSINESS_QUEUE, true, false, false, arg);
channel.queueBind(Config.BUSINESS_QUEUE,Config.BUSINESS_EXCHANGE,Config.BUSINESS_ROUTING_KEY);
/**
* 死信Exchange和Queue
*/
channel.exchangeDeclare(Config.DEAD_EXCHANGE, BuiltinExchangeType.DIRECT,true);
channel.queueDeclare(Config.DEAD_QUEUE,true,false,false,new HashMap<>());
channel.queueBind(Config.DEAD_QUEUE,Config.DEAD_EXCHANGE,Config.DEAD_ROUTING_KEY);
使用測試
生產者發送消息
byte[] msg1 = "ok".getBytes(StandardCharsets.UTF_8);
channel.basicPublish(Config.BUSINESS_EXCHANGE,Config.BUSINESS_ROUTING_KEY,new AMQP.BasicProperties(),msg1);
for (int i = 0; i < 5; i++) {
byte[] msg = String.format("死信消息[%d]",i).getBytes(StandardCharsets.UTF_8);
channel.basicPublish(Config.BUSINESS_EXCHANGE,Config.BUSINESS_ROUTING_KEY,new AMQP.BasicProperties(),msg);
}
我們向正常的業務Exchange發送了6條消息,消息的內容如下:
ok
死信消息[0]
死信消息[1]
死信消息[2]
死信消息[3]
死信消息[4]
消費者消費消息
//業務隊列消息消費
channel.basicConsume(Config.BUSINESS_QUEUE,false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, StandardCharsets.UTF_8);
long deliveryTag = envelope.getDeliveryTag();
if (Objects.equals("ok",message)){
channel.basicAck(deliveryTag,false);
log.info("消息正常簽收");
}else {
//requeue參數需要設置成false,表示不再進入原隊列。如果為false則不會進入死信隊列
channel.basicNack(deliveryTag,false,false);
log.info("消息未簽收,進入到死信隊列,消息內容:{}",message);
}
}
});
//死信隊列消息消費
channel.basicConsume(Config.DEAD_QUEUE,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, StandardCharsets.UTF_8);
log.info("死信隊列收到消息內容:{}",message);
}
});
上面是業務隊列和死信隊列消費的主要代碼。對于業務隊列收到的消息,如果消息內容是ok,我們則認為消費成功。而對于其他內容的消息,我們都認為消費失敗,在手動ACK時我們都選擇了NACK讓其進入到死信隊列。這里面的一個關鍵點在于消息被NACK了且它的requeue參數為false。如果requeue設置為true,它并不會進入到死信隊列的,這一點需要知道。
最后運行的打印結果如下:
14:47:34.425 [pool-1-thread-3] INFO com.buydeem.share.rabbit.ConsumerApp - 消息正常簽收
14:47:34.428 [pool-1-thread-3] INFO com.buydeem.share.rabbit.ConsumerApp - 消息未簽收,進入到死信隊列,消息內容:死信消息[0]
14:47:34.429 [pool-1-thread-3] INFO com.buydeem.share.rabbit.ConsumerApp - 消息未簽收,進入到死信隊列,消息內容:死信消息[1]
14:47:34.430 [pool-1-thread-3] INFO com.buydeem.share.rabbit.ConsumerApp - 消息未簽收,進入到死信隊列,消息內容:死信消息[2]
14:47:34.430 [pool-1-thread-3] INFO com.buydeem.share.rabbit.ConsumerApp - 消息未簽收,進入到死信隊列,消息內容:死信消息[3]
14:47:34.430 [pool-1-thread-3] INFO com.buydeem.share.rabbit.ConsumerApp - 消息未簽收,進入到死信隊列,消息內容:死信消息[4]
14:47:34.431 [pool-1-thread-4] INFO com.buydeem.share.rabbit.ConsumerApp - 死信隊列收到消息內容:死信消息[0]
14:47:34.431 [pool-1-thread-4] INFO com.buydeem.share.rabbit.ConsumerApp - 死信隊列收到消息內容:死信消息[1]
14:47:34.431 [pool-1-thread-4] INFO com.buydeem.share.rabbit.ConsumerApp - 死信隊列收到消息內容:死信消息[2]
14:47:34.431 [pool-1-thread-4] INFO com.buydeem.share.rabbit.ConsumerApp - 死信隊列收到消息內容:死信消息[3]
14:47:34.431 [pool-1-thread-4] INFO com.buydeem.share.rabbit.ConsumerApp - 死信隊列收到消息內容:死信消息[4]
從結果可以看出生產者發送的6條消息中,我們的消費者只成功的處理了一條,而其他5條最后都進入了死信隊列,被死信隊列中的消費者消費了。
消息進入死信隊列的變化
原本正常的業務消息進入到死信隊列之后它的消息體內容并不會發生變化,但是消息的頭部會發生變化。下面我直接通過RabbitMQ的后臺管理端來進行下面的測試:
-
首先我們先Exchange中發送消息
exchange發送消息.png -
接下來在業務隊列中獲取消息
不同的策略最后的處理邏輯.png 消息進入到死信隊列再次獲取消息
對比消息在業務隊列和死信隊列中的數據我們可以發現在死信隊列中的消息頭部多出來需要內容,這部分內容主要記載了該消息原始的一些Exchange、Queue、Routing Key等相關信息,同時還有因為什么原因進入到死信對了等等。
總結
簡單的概括來說死信隊列有點像一個兜底機制,對于最初我們說的那三種情況的一個保底。當消息遇見上面三種情況,我們可以通過業務隊列中設置的x-dead-letter相關參數讓消息進入到別的隊列中。如果不使用死信隊列我們自己也能實現死信隊列同樣的效果,只不過需要我們在遇見上面的情況時手動的將消息推送到其他隊列,這樣做會比較麻煩。而死信隊列正式解決了這個麻煩,讓我們可以通過簡單配置完成上面業務。