RabbitMQ
中的死信交换器(dead letter exchange)可以接收下面三种场景中的消息:
basicReject
或者basicNack
回复,并且requeue
参数设置为false
,即不再将该消息重新在消费者间进行投递.TTL
属性.死信交换器可以在程序中设置,也可以使用rabbitmqctl
工具进行设置.关于死信交换器的介绍请参考RabbitMQ
官网https://www.rabbitmq.com/dlx.html.
死信交换器可以统计或者监控错误消息, 下面使用RabbitMQ的Java
客户端库给出一个完整的例子, 结合第一种消息来源说明死信交换器的使用.
示例代码
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Envelope;
public class DeadLetterExchange {
private static final String NORMAL_EXCHANGE = "normal_exchange";
private static final String DEAD_LETTER_EXCHANGE = "dead_letter_exchange";
private static final String DEFAULT_WORKER_QUEUE = "queue_to_normal_exchange";
private static final String DLX_WORKER_QUEUE = "queue_to_deadletter_exchange";
private static final String NORMAL_ROUTE_KEY = "normal_route_key";
private static final String DLX_ROUTE_KEY = "dlx_route_key";
private Connection connection;
// For consumer and producer schedule.
private final ExecutorService executorService;
private static class SimpleThreadFactory implements ThreadFactory {
private final AtomicInteger idx = new AtomicInteger(0);
public Thread newThread(Runnable r) {
return new Thread(r, "Thread#" + idx.getAndIncrement());
}
}
private static class WorkerConsumer implements Runnable {
private final Connection connection;
private Channel channel;
private String queue;
private final DeliverCallback deliverCallback = (consumerTag, delivery) -> {
final String message = new String(delivery.getBody(), "UTF-8");
final Envelope envelope = delivery.getEnvelope();
final long deliveryTag = envelope.getDeliveryTag();
final boolean redelivery = envelope.isRedeliver();
final BasicProperties props = delivery.getProperties();
final String msgId = props.getMessageId();
// Reject some message and not requeue it.
final boolean reject = (deliveryTag % 3) == 2;
System.out.println("Received normal exchange msg:" + message
+ " msg id:" + msgId
+ " reject:" + reject);
if (reject ) {
channel.basicReject(deliveryTag, false);
} else {
channel.basicAck(deliveryTag, false);
}
};
public WorkerConsumer(Connection connection, String queue) {
this.connection = connection;
this.queue = queue;
}
@Override
public void run() {
try {
channel = connection.createChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE, "direct");
final Map args = new HashMap<>();
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
if (queue == null) {
queue = DEFAULT_WORKER_QUEUE;
}
channel.queueDeclare(queue, false, false, true, args);
channel.queueBind(queue, NORMAL_EXCHANGE, NORMAL_ROUTE_KEY);
final String tag = channel.basicConsume(queue, false, deliverCallback, consumerTag -> {System.out.println("Cancel worker consumer:" +consumerTag);});
System.out.println("worker consumer tag:" + tag);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// This special consumer handle messages delivered to dead letter exchange.
private static class DLXConsumer implements Runnable {
private final Connection connection;
private final DeliverCallback deliverCallback = (consumerTag, delivery) -> {
final String message = new String(delivery.getBody(), "UTF-8");
final BasicProperties props = delivery.getProperties();
final String msgId = props.getMessageId();
System.out.println("Received dead letter exchange msg:" + message
+ " msg id:" + msgId);
};
public DLXConsumer(Connection connection) {
this.connection = connection;
}
@Override
public void run() {
try {
final Channel channel = connection.createChannel();
channel.exchangeDeclare(DEAD_LETTER_EXCHANGE, "direct");
channel.queueDeclare(DLX_WORKER_QUEUE, false, false, true, null);
channel.queueBind(DLX_WORKER_QUEUE, DEAD_LETTER_EXCHANGE, NORMAL_ROUTE_KEY);
final String tag = channel.basicConsume(DLX_WORKER_QUEUE, true, deliverCallback, consumerTag -> {System.out.println("Cancel DLX consumer:" +consumerTag);});
System.out.println("dlx consumer tag:" + tag);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static class Producer implements Runnable {
private static final int MSG_COUNTS = 20;
private final Connection connection;
public Producer(Connection connection) {
this.connection = connection;
}
@Override
public void run() {
try {
final Channel channel = connection.createChannel();
// Send some messages, some of them can not route to any queue.
for (int i = 0; i < MSG_COUNTS; i++) {
final BasicProperties.Builder propsBuilder = new BasicProperties.Builder();
final BasicProperties props = propsBuilder.appId(Thread.currentThread().toString()).messageId(String.valueOf(i)).build();
final boolean useInvalidRouteKey = (i % 4 == 3);
if (!useInvalidRouteKey) {
channel.basicPublish(NORMAL_EXCHANGE, NORMAL_ROUTE_KEY, props, "[normal message.]".getBytes());
} else {
channel.basicPublish(NORMAL_EXCHANGE, "not_existed_route_key", props, "[lost message.]".getBytes());
}
}
} catch(IOException e) {
e.printStackTrace();
}
}
}
public DeadLetterExchange() {
executorService = Executors.newCachedThreadPool(new SimpleThreadFactory());
final ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
try {
connection = connectionFactory.newConnection();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
public void execWorkderConsumer(String queue) {
executorService.submit(new WorkerConsumer(connection, queue));
}
public void execDLXConsumer() {
executorService.submit(new DLXConsumer(connection));
}
public void execProducer(int n) {
for (int i = 0; i < n; i++) {
executorService.submit(new Producer(connection));
}
}
public void clearConfig(String[] queues) {
try {
final Channel channel = connection.createChannel();
channel.exchangeDelete(DEAD_LETTER_EXCHANGE);
channel.exchangeDelete(NORMAL_EXCHANGE);
channel.queueDelete(DEFAULT_WORKER_QUEUE);
channel.queueDelete(DLX_WORKER_QUEUE);
for (String queue : queues) {
channel.queueDelete(queue);
}
System.out.println("Config cleared.");
System.exit(0);
} catch(IOException e) {
e.printStackTrace();
}
}
public static void main(String args[]) {
if (args.length > 0) {
final String which = args[0];
if ("consumer".equals(which)) {
final String workQueue = (args.length > 1 ? args[1] : null);
new DeadLetterExchange().execWorkderConsumer(workQueue);
} else if ("producer".equals(which)) {
final int n = (args.length > 1 ? Integer.parseInt(args[1]) : 1);
new DeadLetterExchange().execProducer(n);
} else if ("dlxconsumer".equals(which)) {
new DeadLetterExchange().execDLXConsumer();
} else if ("clearconfig".equals(which)) {
String[] queues = new String[0];
if (args.length > 1) {
queues = new String[args.length - 1];
System.arraycopy(args, 1, queues, 0, args.length - 1);
}
new DeadLetterExchange().clearConfig(queues);
}
}
}
}
其中,producer
投递消息的交换器定义为NORMAL_EXCHANGE, 其consumer
实现定义为WorkerConsumer
, 死信交换器定义为DEAD_LETTER_EXCHANGE, 其consumer
实现定义为DLXConsumer
.
示例代码中,可以为NORMAL_EXCHANGE绑定实现一个消息队列, 可以运行多个consumer在该队列中以Round-robin
的方式处理消息, 也可以为NORMAL_EXCHANGE绑定实现多个队列.通常死信交换器上的消息处理负载可能并不高,可以用一个consumer来处理其消息, 当然为NORMAL_EXCHANGE绑定多个队列或者运行多个consumer也是可以的. 使用死信交换器时, 其架构如下(使用一个worker consumer队列的情况):
首先, 将该类导出为Runnable JAR
包.打开一个控制台终端,运行一个worker consumer
:
java -jar deadletterexchange.jar consumer
打开另一个控制台终端, 运行一个DLX consumer
:
java -jar deadletterexchange.jar dlxconsumer
打开另一个控制台终端, 运行一个producer
:
java -jar deadletterexchange.jar producer
producer
发送完消息后, worker consumer
的log输出为:
Received normal exchange msg:[normal message.] msg id:0 reject:false
Received normal exchange msg:[normal message.] msg id:1 reject:true
Received normal exchange msg:[normal message.] msg id:2 reject:false
Received normal exchange msg:[normal message.] msg id:4 reject:false
Received normal exchange msg:[normal message.] msg id:5 reject:true
Received normal exchange msg:[normal message.] msg id:6 reject:false
Received normal exchange msg:[normal message.] msg id:8 reject:false
Received normal exchange msg:[normal message.] msg id:9 reject:true
Received normal exchange msg:[normal message.] msg id:10 reject:false
Received normal exchange msg:[normal message.] msg id:12 reject:false
Received normal exchange msg:[normal message.] msg id:13 reject:true
Received normal exchange msg:[normal message.] msg id:14 reject:false
Received normal exchange msg:[normal message.] msg id:16 reject:false
Received normal exchange msg:[normal message.] msg id:17 reject:true
Received normal exchange msg:[normal message.] msg id:18 reject:false
DLX consumer
的log输出为:
Received dead letter exchange msg:[normal message.] msg id:1
Received dead letter exchange msg:[normal message.] msg id:5
Received dead letter exchange msg:[normal message.] msg id:9
Received dead letter exchange msg:[normal message.] msg id:13
Received dead letter exchange msg:[normal message.] msg id:17
在worker consumer
中, 对部分消息使用了basicReject
进行拒绝回复, 并且其requeue
属性为false
. 从worker consumer的log输出看,这些被拒绝的消息ID为:1,5,9,13,17. 再来看DLX consumer
的log输出,发现worker consumer上这些被拒绝回复的消息全部路由到了DLX
交换器上了.
对于死信交换器需要注意的几个细节是:
direct
类型).唯一要做的是在定义worker consumer
的队列时, 使用参数x-dead-letter-exchange
指定死信交换器即可.worker consumer
中定义队列指定死信交换器时, 该死信交换器甚至可以不存在(即还没有定义). 示例中先在终端中运行了worker consumer,然后运行了DLX consumer. 在运行worker consumer时, 死信交换器还没有被定义.但是当有消息需要被路由到死信交换器时, 其必须已经存在.worker consumer
可以使用basicReject或者basicNack对消息进行确认, 当这两个命令的requeue参数设置为false时, 该消息才被路由到死信交换器.否则, 消息将被重新在worker consumer间进行投递. 需要注意的是, 重新投递的消息可能还会被投递到拒绝该消息的worker consumer上, 形成循环.producer
发送消息时, 如果该消息无法路由到任何队列, 那么这个消息是不会被路由到死信交换器的. 示例中producer发送的部分消息,其route key
为not_existed_route_key, 这些消息无法路由到任何队列, 也不会被路由到死信交换器.producer
发送时的route key
. 但是, 可以改变路由到死信交换器的route key. 在worker consumer
定义其队列时, 除了使用x-dead-letter-exchange
参数指定死信交换器, 还可以使用x-dead-letter-routing-key
参数指定该消息的route key
.使用这个配置可以在DLX worker
中判断出消息是从哪个worker consumer
路由过来的.BTW: 因为这个示例导出的Runnable JAR
包中包含了RabbitMQ的Java客户端库,所以可以直接运行. 否则,运行时需要在classpath
中指定其客户端库路径.