RabbitMQ 消息丢失问题

107 阅读3分钟

一、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);
    }
}

三、问题描述

  1. 预期行为:生产者发送的订单消息能可靠地被消费者接收并处理,不会出现消息丢失的情况。

  2. 实际行为:在某些情况下,如生产者发送消息后 RabbitMQ 服务器重启,或者消费者处理消息过程中突然崩溃,消息可能会丢失。

    • 生产者端:在上述生产者代码中,basicPublish 方法调用后并没有确认机制来确保消息已被 RabbitMQ 服务器正确接收。如果在消息发送到服务器但还未被持久化时服务器重启,消息就会丢失。
    • 消费者端:在消费者代码中,basicConsume 方法的第二个参数 autoAck 设置为 true,这意味着消费者一旦接收到消息,RabbitMQ 就会自动将其从队列中删除。如果消费者在处理消息过程中崩溃,还未处理完的消息就会丢失。

四、解决方案

生产者端

  1. 开启发布确认机制

    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 服务器。

消费者端

  1. 关闭自动确认,手动确认消息

    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 服务器重启后队列和消息依然存在。