在RabbiteMQ消息的拒絕 —— Reject和Nack中,我們是可以利用Reject和Nack去拒絕消息的,消息在被拒絕的時候是可以指定RabbitMQ是否需要重新發送給消費者。
如何我們消息被拒絕,且不重新投遞的情況下,那麼這條消息就無法被處理了麼?在ActiveMQ死信隊列中,我們介紹過消息失敗被重發一定次數後會進入死信隊列,在RabbitMQ中也有類似的處理——死信交換器。
死信交換器不是默認的設置,這裏是被投遞消息被拒絕後的一個可選行爲,是在創建隊列的時進行聲明的,往往用在對問題消息的診斷上。
消息進入死信交換器一般是以下幾種情況:
- 消息被拒絕,並且設置
requeue
參數爲 false - 消息過期
- 隊列達到最大長度
死信交換器仍然只是一個普通的交換器,創建時並沒有特別要求和操作。在創建隊列的時候,聲明該交換器將用作保存被拒絕的消息即可,相關的參數是x-dead-letter-exchange
。
死信交換器的設置其實和RabbitMQ消息發佈之備用交換器比較類似,不過備用交換器是消息生產者在發佈消息時無法路由消息,消息將被路由到備用交換器,所以備用交換器配置在消息的生產者。而死信交換器則是接收過期或者被拒絕的消息,所以肯定是配置在消息的消費者。
首先我們先看看消息的發佈者,這裏和之前介紹的用法一樣,就是發佈了error、warn、info三條消息,如下:
public class Producer {
//交換器名稱
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception {
//創建連接,連接到RabbitMQ
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
Connection connection = connectionFactory.newConnection();
//創建信道
Channel channel = connection.createChannel();
//創建交換器
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, false, false, null);
//定義的業務日誌消息級別,即作爲路由鍵使用
String[] logLevels = {"error", "warn", "info"};
for (String logLevel : logLevels) {
String msg = "Hello RabbitMQ";
//發佈消息,需要參數:交換器、路由鍵,其中以日誌消息級別爲路由鍵
channel.basicPublish(EXCHANGE_NAME, logLevel, null, msg.getBytes(Charset.forName("UTF-8")));
}
channel.close();
connection.close();
}
}
然後我們給生產者生成的三條消息,一共創建了2個消費者,一個是可以接收到全部的消息,並且全部拒絕並不重新投遞,其中我們就需要配置其死信交換器了,如下:
public class AllConsumer {
public static final String EXCHANGE_NAME = "logs";
public static final String DLX_EXCHANGE_NAME = "dlx";
public static void main(String[] args) throws Exception {
//創建連接,連接到RabbitMQ,與發送端一樣
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
Connection connection = connectionFactory.newConnection();
//創建信道
Channel channel = connection.createChannel();
//創建交換器
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, false, false, null);
String queueName = "allLog";
Map<String, Object> argMap = new HashMap<>();
argMap.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
//創建一個隊列
channel.queueDeclare(queueName, false, false, false, argMap);
//將隊列和交換器通過路由鍵進行綁定
channel.queueBind(queueName, EXCHANGE_NAME, "#");
//聲明瞭一個消費者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = "Exchange: " + envelope.getExchange() + ", " +
"RoutingKey: " + envelope.getRoutingKey() + ", " +
"Content: " + new String(body, "UTF-8");
System.out.println(message);
channel.basicReject(envelope.getDeliveryTag(), false);
}
};
//消費者正式開始在指定隊列上消費消息
channel.basicConsume(queueName, false, consumer);
}
}
另外一個消費者,我們只接收其error消息,並且也是拒絕並不重新投遞,不同的是我們將其送到死信交換器的時候,將其路由鍵進行重新定義了(死信交換器重新定義路由鍵,可以將其不同的消息,發送到同一個隊列之類,可以進行統一處理失敗的消息),代碼如下:
public class ErrorConsumer {
public static final String EXCHANGE_NAME = "logs";
public static final String DLX_EXCHANGE_NAME = "dlx";
public static void main(String[] args) throws Exception {
//創建連接,連接到RabbitMQ,與發送端一樣
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
Connection connection = connectionFactory.newConnection();
//創建信道
Channel channel = connection.createChannel();
//創建交換器
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, false, false, null);
String queueName = "errorLog";
Map<String, Object> argMap = new HashMap<>();
argMap.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
//死信路由鍵,會替換消息原來的路由鍵
argMap.put("x-dead-letter-routing-key", "log_error");
//創建一個隊列
channel.queueDeclare(queueName, false, false, false, argMap);
//將隊列和交換器通過路由鍵進行綁定
channel.queueBind(queueName, EXCHANGE_NAME, "error");
//聲明瞭一個消費者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = "Exchange: " + envelope.getExchange() + ", " +
"RoutingKey: " + envelope.getRoutingKey() + ", " +
"Content: " + new String(body, "UTF-8");
System.out.println(message);
channel.basicReject(envelope.getDeliveryTag(), false);
}
};
//消費者正式開始在指定隊列上消費消息
channel.basicConsume(queueName, false, consumer);
}
}
上述兩個消費者使用的是同一個死信交換器——dlx,兩個消費者失敗的消息都會進入同一個死信交換器中。當然我們也可以爲每個消費者都定義一個單獨的死信交換器,這樣多個消費者失敗的消息就會進入不同的死信交換器,我們在處理的時候就需要對多個死信交換器進行處理,這裏就需要看其業務的要求的。
另外上述我們發現我們還沒有創建死信交換器(當然我們也是可以在兩個消費者中進行創建的,只要保證其參數相同),這裏我們就是其死信交換器的消費者中進行創建的,這裏同樣我們爲死信交換器也創建了兩個消費者,一個可以消費其中的所有消息,如下:
public class AllDlxConsumer {
public static final String DLX_EXCHANGE_NAME = "dlx";
public static void main(String[] args) throws Exception {
//創建連接,連接到RabbitMQ,與發送端一樣
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
Connection connection = connectionFactory.newConnection();
//創建信道
Channel channel = connection.createChannel();
//創建交換器
channel.exchangeDeclare(DLX_EXCHANGE_NAME, BuiltinExchangeType.TOPIC, false, false, null);
String queueName = channel.queueDeclare().getQueue();
//將隊列和交換器通過路由鍵進行綁定
channel.queueBind(queueName, DLX_EXCHANGE_NAME, "#");
//聲明瞭一個消費者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = "Exchange: " + envelope.getExchange() + ", " +
"RoutingKey: " + envelope.getRoutingKey() + ", " +
"Content: " + new String(body, "UTF-8");
System.out.println(message);
}
};
//消費者正式開始在指定隊列上消費消息
channel.basicConsume(queueName, true, consumer);
}
}
另一個死信交換器消費者只用來處理那個被我們修改了路由鍵的error消息,上述中我們將其路由鍵有error修改爲了log_error,其代碼如下:
public class ErrorDlxConsumer {
public static final String DLX_EXCHANGE_NAME = "dlx";
public static void main(String[] args) throws Exception {
//創建連接,連接到RabbitMQ,與發送端一樣
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
Connection connection = connectionFactory.newConnection();
//創建信道
Channel channel = connection.createChannel();
//創建交換器
channel.exchangeDeclare(DLX_EXCHANGE_NAME, BuiltinExchangeType.TOPIC, false, false, null);
String queueName = channel.queueDeclare().getQueue();
//將隊列和交換器通過路由鍵進行綁定
channel.queueBind(queueName, DLX_EXCHANGE_NAME, "log_error");
//聲明瞭一個消費者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = "Exchange: " + envelope.getExchange() + ", " +
"RoutingKey: " + envelope.getRoutingKey() + ", " +
"Content: " + new String(body, "UTF-8");
System.out.println(message);
}
};
//消費者正式開始在指定隊列上消費消息
channel.basicConsume(queueName, true, consumer);
}
}
然後我運行上述所有代碼,其結果如下,所有消費者受到error、warn、info所有消息,其實都是拒絕的
單獨處理error消息的消費者,只收到其error消息,並拒絕其消息,將其路由鍵修改爲log_error
上述四條消息拒絕都是正常的,所以都進入其死信交換器,我們先查看其消費所有死信交換器的消費者,如下
剩下的死信交換器消費者應該只是消費log_error的消息,如下: