一、Bug 场景
在一个电商系统中,使用 RabbitMQ 来处理订单相关的异步消息。例如,当用户下单后,系统会发送一条订单创建的消息到 RabbitMQ,由专门的消费者服务来处理订单后续的操作,如库存扣减、订单状态更新等。
二、代码示例
生产者代码(简化的 Java 示例)
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
public class OrderProducer {
private static final String QUEUE_NAME = "order_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "新订单创建";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
消费者代码(简化的 Java 示例)
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP;
public class OrderConsumer {
private static final String QUEUE_NAME = "order_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
channel.basicConsume(QUEUE_NAME, true,
"orderConsumerTag",
(consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
// 模拟订单处理业务逻辑
processOrder(message);
},
consumerTag -> {
System.out.println("Consumer cancelled: " + consumerTag);
});
}
private static void processOrder(String message) {
// 模拟订单处理
System.out.println("订单处理中: " + message);
}
}
三、问题描述
-
预期行为:生产者发送的订单消息能可靠地被消费者接收并处理,不会出现消息丢失的情况。
-
实际行为:在某些情况下,如生产者发送消息后 RabbitMQ 服务器重启,或者消费者处理消息过程中突然崩溃,消息可能会丢失。
- 生产者端:在上述生产者代码中,
basicPublish方法调用后并没有确认机制来确保消息已被 RabbitMQ 服务器正确接收。如果在消息发送到服务器但还未被持久化时服务器重启,消息就会丢失。 - 消费者端:在消费者代码中,
basicConsume方法的第二个参数autoAck设置为true,这意味着消费者一旦接收到消息,RabbitMQ 就会自动将其从队列中删除。如果消费者在处理消息过程中崩溃,还未处理完的消息就会丢失。
- 生产者端:在上述生产者代码中,
四、解决方案
生产者端
-
开启发布确认机制:
import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Connection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.ConfirmCallback; public class OrderProducer { private static final String QUEUE_NAME = "order_queue"; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { channel.queueDeclare(QUEUE_NAME, true, false, false, null); channel.confirmSelect(); channel.addConfirmListener((sequenceNumber, multiple) -> { if (multiple) { System.out.println("Multiple messages confirmed up to sequence number: " + sequenceNumber); } else { System.out.println("Message with sequence number: " + sequenceNumber + " confirmed"); } }, (sequenceNumber, multiple) -> { System.out.println("Message(s) with sequence number(s) up to: " + sequenceNumber + " were not confirmed"); }); String message = "新订单创建"; channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8")); System.out.println(" [x] Sent '" + message + "'"); } } }上述代码中,通过
channel.confirmSelect()开启发布确认机制,并添加确认监听器,确保消息成功发送到 RabbitMQ 服务器。
消费者端
-
关闭自动确认,手动确认消息:
import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Connection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import com.rabbitmq.client.AMQP; public class OrderConsumer { private static final String QUEUE_NAME = "order_queue"; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME, true, false, false, null); System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); channel.basicConsume(QUEUE_NAME, false, "orderConsumerTag", (consumerTag, delivery) -> { String message = new String(delivery.getBody(), "UTF-8"); System.out.println(" [x] Received '" + message + "'"); try { // 模拟订单处理业务逻辑 processOrder(message); channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); } catch (Exception e) { channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true); } }, consumerTag -> { System.out.println("Consumer cancelled: " + consumerTag); }); } private static void processOrder(String message) { // 模拟订单处理 System.out.println("订单处理中: " + message); } }在上述代码中,将
basicConsume的autoAck设置为false,消费者处理完消息后手动调用basicAck确认消息,若处理失败则调用basicNack并根据情况决定是否重新入队,从而避免消息丢失。同时,将队列声明为持久化(queueDeclare的第一个参数设为true),保证 RabbitMQ 服务器重启后队列和消息依然存在。