MQ的消息可靠性
在使用消息队列(MQ)时。如果消息消费失败,且重试后仍然失败,就需要进行手动介入处理,以避免消息丢失或业务逻辑错误。这种情况下,通常会将失败的消息存储到一个“死信队列”(Dead Letter Queue,DLQ)中,方便后续手动处理。
场景描述
假设我们有一个电商平台,用户下单后会将订单消息发送到消息队列中,由订单处理服务消费这些消息并完成订单处理。如果订单处理服务在消费消息时失败(例如,数据库连接失败、库存不足等),并且重试后仍然失败,就需要将这些消息发送到死信队列中,以便人工介入处理。
死信队列(DLQ)的实现
RabbitMQ 提供了死信队列的功能,可以通过设置队列的死信交换器(Dead Letter Exchange,DLX)来实现。当消息消费失败且达到最大重试次数后,消息会被自动发送到死信队列中。
示例代码
1. 定义队列和交换器
定义普通队列和死信队列,并设置死信交换器。
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
public class DeadLetterQueueExample {
private static final String EXCHANGE_NAME = "order_exchange";
private static final String QUEUE_NAME = "order_queue";
private static final String DLX_NAME = "dlx_exchange";
private static final String DLQ_NAME = "dlq_queue";
public static void setupQueues() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 创建普通交换器和队列
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "order_routing_key");
// 创建死信交换器和队列
channel.exchangeDeclare(DLX_NAME, BuiltinExchangeType.DIRECT, true);
channel.queueDeclare(DLQ_NAME, true, false, false, null);
channel.queueBind(DLQ_NAME, DLX_NAME, "dlq_routing_key");
// 设置普通队列的死信交换器
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", DLX_NAME);
args.put("x-dead-letter-routing-key", "dlq_routing_key");
channel.queueDeclare(QUEUE_NAME, true, false, false, args);
}
}
}
2. 生产者代码
发送消息到普通队列。
public class Producer {
private static final String EXCHANGE_NAME = "order_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
String message = "Order 12345";
channel.basicPublish(EXCHANGE_NAME, "order_routing_key", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
3. 消费者代码
消费消息,如果失败则触发重试,达到最大重试次数后将消息发送到死信队列。
public class Consumer {
private static final String QUEUE_NAME = "order_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
try {
// 模拟消息处理逻辑
if (message.equals("Order 12345")) {
System.out.println(" [x] Processing failed for '" + message + "'");
throw new RuntimeException("Processing failed");
} else {
System.out.println(" [x] Processing succeeded for '" + message + "'");
}
} catch (Exception e) {
System.out.println(" [x] Re-queueing message...");
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true);
}
};
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> {});
}
}
4. 死信队列消费者代码
手动介入处理死信队列中的消息。
public class DeadLetterConsumer {
private static final String DLQ_NAME = "dlq_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received dead letter message: '" + message + "'");
// 手动介入处理逻辑
System.out.println(" [x] Manually processing message...");
};
channel.basicConsume(DLQ_NAME, true, deliverCallback, consumerTag -> {});
}
}
运行流程
- 启动队列和交换器:运行
DeadLetterQueueExample.setupQueues()
,初始化普通队列和死信队列。 - 发送消息:运行
Producer
,发送消息到普通队列。 - 消费消息:运行
Consumer
,消费消息。如果消息处理失败,会触发重试。如果重试后仍然失败,消息会被发送到死信队列。 - 处理死信队列:运行
DeadLetterConsumer
,手动介入处理死信队列中的消息。