RabbitMQ 可靠性投递

文章目录


前言

RabbitMQ 是一个流行的消息队列系统,用于在分布式系统中传递消息。其中一个重要特性是其可靠性投递(Reliable Message Delivery),保证消息在队列和消费者之间可靠的传递和处理。


一、RabbitMQ自带机制

RabbitMQ 的架构图

从架构图中,我们可以发现,消息投递的关键步骤在于如下四点

  • 1.生产者发送消息
  • 2.消息路由机制
  • 3.消息存储持久化机制
  • 4.消费者消费消息

接下来我们一步一步进行分析

1、生产者发送消息

当网络中断或者节点不存在等,均有可能导致生产者发送消息失败,对于失败,可以通过如下机制进行处理

  • 事务(Transactions)
  • 发布确认(Publisher Confirms)

事务(Transactions)和发布确认(Publisher Confirms)是两种确保消息持久性和可靠性的方法。

注意

事务提供了一种全有或全无的机制,但通常不建议在生产环境中使用,因为它们会显著降低性能。发布确认则提供了更轻量级的解决方案,具有更高的性能和灵活性。

1.1、事务(Transactions)

事务的工作原理

  • 事务机制确保一组消息的发送要么全部成功,要么全部失败。如果提交事务失败,所有消息都会回滚,确保数据一致性。

使用方法

  • 启动事务:在信道上启动事务模式。
  • 提交事务:成功时提交所有消息。
  • 回滚事务:发生错误时回滚所有消息。

示例

java 复制代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class RabbitMQTransaction {

    private final static String QUEUE_NAME = "transaction_queue";

    public static void main(String[] args) 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);
            
            try {
                // 启动事务模式
                channel.txSelect();

                for (int i = 0; i < 10; i++) {
                    String message = "Message " + i;
                    channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
                    System.out.println("Sent: " + message);
                }

                // 提交事务
                channel.txCommit();
                System.out.println("Transaction committed");

            } catch (Exception e) {
                System.out.println("Transaction failed: " + e.getMessage());
                // 回滚事务
                channel.txRollback();
                System.out.println("Transaction rolled back");
            }
        }
    }
}

1.2、发布确认(Publisher Confirms)

1.2.1、同步

相较于事务,发布确认(Publisher Confirms)是更为推荐的方式,因为它可以提供类似的可靠性并且具有更好的性能表现:

  • 发布:使用 channel.confirmSelect() 启用发布确认模式。
  • 回调:在发布消息后调用 channel.waitForConfirmsOrDie 方法等待确认,或捕获 Exception 进行错误处理。
java 复制代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.ConfirmCallback;

public class RabbitMQPublishConfirm {

    private final static String QUEUE_NAME = "confirm_queue";

    public static void main(String[] args) 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();

            // 发布消息
            for (int i = 0; i < 10; i++) {
                String message = "Message " + i;
                channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
                System.out.println("Sent: " + message);
            }

            // 确认所有发布的消息
            // 批量确认结果, ACK 如果是 Multiple=True, 代表 ACK 里面的 Delivery-Tag 之前的消息都被确认了
            // 比如 5 条消息可能只收到 1 个 ACK, 也可能收到 2 个(抓包才看得到)
            // 直到所有信息都发布, 只要有一个未被 Broker 确认就会 Exception
            channel.waitForConfirmsOrDie(5000);
            System.out.println("All messages confirmed");
        } catch (Exception e) {
            System.err.println("Message publishing failed: " + e.getMessage());
        }
    }
}
1.2.2、异步

异步确认模式(Asynchronous Confirmations)在发布确认模式的基础上提供了更高效和灵活的消息确认机制。相比于同步发布确认,异步模式允许发布者继续发送消息而不必等待每条消息的确认。这种模式更适合高吞吐量的生产环境。以下是实现异步确认模式的详细步骤和示例代码:

  • 建立连接和信道:与RabbitMQ服务器建立连接并创建信道。
  • 启用发布确认模式:使用 channel.confirmSelect() 方法。
  • 设置确认回调(Confirms Callback):定义消息确认和未确认的回调方法。
    • ConfirmCallback: 用于确认成功的回调。
    • ConfirmListener: 用于设定成功和失败的回调。
  • 发送消息:发布者继续发送消息,不需要等待确认。
java 复制代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.ConfirmCallback;

public class AsyncConfirmPublisher {

    private static final String QUEUE_NAME = "async_confirm_queue";

    public static void main(String[] args) 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();

            // 设置确认和未确认的回调处理事件
            ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
                System.out.println("Message ACKed with delivery tag: " + deliveryTag + ", multiple: " + multiple);
            };

            ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
                System.err.println("Message NACKed with delivery tag: " + deliveryTag + ", multiple: " + multiple);
            };

            channel.addConfirmListener(ackCallback, nackCallback);

            // 发布消息
            for (int i = 0; i < 10; i++) {
                String message = "Message " + i;
                channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
                System.out.println("Sent: " + message);
            }
        }
    }
}

2、消息路由机制

当 routingKey 错误,或者队列不存在的时候,就会出现无法路由,导致消息投递失败,解决方案

  • 使用备份交换机(Alternate Exchanges):如果消息无法路由到指定的交换机,RabbitMQ 可以将消息路由到一个备用交换机。
  • 启用消息的确认回调(Return Callbacks):当消息无法路由到任何队列时,可以使用回调函数捕获并处理这些未被路由的消息。
  • 配置死信交换机(Dead Letter Exchanges):当消息在队列中无法被消费或出现错误时,可将消息转发到死信交换机进行之后处理。

2.1、使用备份交换机(Alternate Exchanges)

备份交换机可以防止消息丢失,如果消息无法路由到主交换机,会被备份交换机存储。

java 复制代码
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    private static final String PRIMARY_EXCHANGE = "primary_exchange";
    private static final String PRIMARY_QUEUE = "primary_queue";
    private static final String ALTERNATE_EXCHANGE = "alternate_exchange";
    private static final String ALTERNATE_QUEUE = "alternate_queue";

    @Bean
    public ConnectionFactory connectionFactory() {
        // Configuration connection factory, assuming default settings
        return new CachingConnectionFactory("localhost");
    }

    @Bean
    public RabbitTemplate rabbitTemplate() {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback(returnCallback);
        return rabbitTemplate;
    }

    @Bean
    public DirectExchange primaryExchange() {
        return ExchangeBuilder.directExchange(PRIMARY_EXCHANGE)
                .durable(true)
                .alternate(ALTERNATE_EXCHANGE)
                .build();
    }

    @Bean
    public DirectExchange alternateExchange() {
        return new DirectExchange(ALTERNATE_EXCHANGE);
    }

    @Bean
    public Queue primaryQueue() {
        return QueueBuilder.durable(PRIMARY_QUEUE).build();
    }

    @Bean
    public Queue alternateQueue() {
        return new Queue(ALTERNATE_QUEUE);
    }

    @Bean
    public Binding bindingPrimary() {
        return BindingBuilder.bind(primaryQueue()).to(primaryExchange()).with("primary_key");
    }

    @Bean
    public Binding bindingAlternate() {
        return BindingBuilder.bind(alternateQueue()).to(alternateExchange()).with("");
    }

    private final RabbitTemplate.ReturnCallback returnCallback = (message, replyCode, replyText,
                                                                  exchange, routingKey) -> {
        // Handle undeliverable message
        System.err.printf("Message returned: %s, code: %d, text: %s, exchange: %s, routing key: %s%n",
                new String(message.getBody()), replyCode, replyText, exchange, routingKey);
    };
}

2.2、启用消息的确认回调(Return Callbacks)

启用消息的确认回调,当消息无法路由到任何队列时,可以使用回调函数捕获并处理这些未被路由的消息。

java 复制代码
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    private static final String PRIMARY_EXCHANGE = "primary_exchange";
    private static final String PRIMARY_QUEUE = "primary_queue";

    @Bean
    public ConnectionFactory connectionFactory() {
        return new CachingConnectionFactory("localhost");
    }

    @Bean
    public RabbitTemplate rabbitTemplate() {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 错误处理逻辑
            System.err.printf("Message returned: %s, code: %d, text: %s, exchange: %s, routing key: %s%n",
                    new String(message.getBody()), replyCode, replyText, exchange, routingKey);
        });
        return rabbitTemplate;
    }

    @Bean
    public DirectExchange primaryExchange() {
        return ExchangeBuilder.directExchange(PRIMARY_EXCHANGE).durable(true).build();
    }

    @Bean
    public Queue primaryQueue() {
        return QueueBuilder.durable(PRIMARY_QUEUE).build();
    }

    @Bean
    public Binding bindingPrimary() {
        return BindingBuilder.bind(primaryQueue()).to(primaryExchange()).with("primary_key");
    }
}

2.3、配置死信交换机(Dead Letter Exchanges)

消息在队列中无法被消费或出现错误时,可将其转发到死信交换机进行特定处理。

java 复制代码
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    private static final String PRIMARY_EXCHANGE = "primary_exchange";
    private static final String PRIMARY_QUEUE = "primary_queue";
    private static final String DLX_EXCHANGE = "dlx_exchange";
    private static final String DLX_QUEUE = "dlx_queue";

    @Bean
    public ConnectionFactory connectionFactory() {
        return new CachingConnectionFactory("localhost");
    }

    @Bean
    public RabbitTemplate rabbitTemplate() {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 错误处理逻辑
            System.err.printf("Message returned: %s, code: %d, text: %s, exchange: %s, routing key: %s%n",
                    new String(message.getBody()), replyCode, replyText, exchange, routingKey);
        });
        return rabbitTemplate;
    }

    @Bean
    public DirectExchange primaryExchange() {
        return new DirectExchange(PRIMARY_EXCHANGE);
    }

    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange(DLX_EXCHANGE);
    }

    @Bean
    public Queue primaryQueue() {
        return QueueBuilder.durable(PRIMARY_QUEUE)
                           .withArgument("x-dead-letter-exchange", DLX_EXCHANGE)
                           .build();
    }

    @Bean
    public Queue dlxQueue() {
        return QueueBuilder.durable(DLX_QUEUE).build();
    }

    @Bean
    public Binding bindingPrimary() {
        return BindingBuilder.bind(primaryQueue()).to(primaryExchange()).with("primary_key");
    }

    @Bean
    public Binding bindingDLX() {
        return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("#");
    }
}

3、消息存储持久化机制

如果消息没有持久化,当RabbitMQ服务重启、节点故障等情况下就会丢失消息,解决方案

3.1、队列持久化(Durable Queue)

创建队列时将其设置为持久化

java 复制代码
channel.queueDeclare("queue_name", durable=True)

3.2、消息持久化(Persistent Message)

发送消息时将其标记为持久化

java 复制代码
channel.basicPublish(exchange='', routingKey='queue_name', body=msg, properties=pika.BasicProperties(delivery_mode=2,))

3.3、集群部署

保证节点的高可用

4、消费者消费消息

在使用 RabbitMQ 作为消息队列系统时,消费者确认机制(Consumer Acknowledge)对于确保消息可靠消费非常重要。当消费者从队列中接收到消息时,必须明确确认消息已被成功处理。这种机制不仅防止消息丢失,还避免了消息重复消费的问题。

RabbitMQ 提供了两种主要的消息确认机制:

  • 手动确认(Manual Acknowledgement):消费者显式地向 RabbitMQ 发送确认消息,表明消息已被成功处理。
  • 自动确认(Automatic Acknowledgement):RabbitMQ 在消息被发送给消费者后立即认为消息已经成功处理,无需等待显式确认。(默认)
java 复制代码
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    private static final String EXCHANGE_NAME = "direct_exchange";
    private static final String QUEUE_NAME = "example_queue";

    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange(EXCHANGE_NAME);
    }

    @Bean
    public Queue queue() {
        return new Queue(QUEUE_NAME, true);
    }

    @Bean
    public Binding binding(Queue queue, DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("routing_key");
    }

    @Bean
    public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
            MessageListenerAdapter listenerAdapter) {

        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames(QUEUE_NAME);
        container.setMessageListener(listenerAdapter);
        // 自动确认 AcknowledgeMode.AUTO
        // 手动确认 AcknowledgeMode.MANUAL
        container.setAcknowledgeMode(org.springframework.amqp.core.AcknowledgeMode.MANUAL); 
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(Consumer consumer) {
        return new MessageListenerAdapter(consumer, "consumeMessage");
    }
}

如果是手动确认,消费者处理消息的时候实现手动确认机制:

复制代码
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;

@Component
public class Consumer implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        try {
            // 处理消息逻辑
            String body = new String(message.getBody());
            System.out.println("Received message: " + body);

            // 手动确认消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            // 消息处理失败,拒绝消息并重新入队
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            System.err.println("Failed to process message: " + e.getMessage());
        }
    }
}

二、业务保证

1、最终一致性

  • 消息持久化处理:在发出消息之前,将消息存储到持久化存储设备,如数据库。 并使用消息状态进行追踪,每个状态表示消息的处理进度。
  • 补偿机制:根据超时和失败记录进行消息补偿。
    • 怎么触发重发(定时任务)
    • 多久触发一次(参考业务属性)
    • 触发多少次(参考业务属性)
  • 幂等性:确保消费者在处理消息时是幂等的,即多次处理不会导致副作用。

通过以上的方案结合,保证消息的最终一致性

2、监控和告警

建立监控系统,及时发现和处理异常情况,然后及时作出相应,增加客户的良好体验

例如:

使用 Prometheus 和 Grafana 对消息队列和数据库进行监控,及时发现并告警异常情况。

也可以结合业务属性,自己创建一套简单的监控系统。

相关推荐
bobz9651 分钟前
ovs patch port 对比 veth pair
后端
Asthenia041211 分钟前
Java受检异常与非受检异常分析
后端
uhakadotcom25 分钟前
快速开始使用 n8n
后端·面试·github
JavaGuide32 分钟前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz96542 分钟前
qemu 网络使用基础
后端
Asthenia04121 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04121 小时前
Spring 启动流程:比喻表达
后端
Asthenia04122 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua2 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫
致心2 小时前
记一次debian安装mariadb(带有迁移数据)
后端