RabbitMQ可靠性:消息传递过程中的可靠性保证

作者:禅与计算机程序设计艺术

1.简介

RabbitMQ(Rabbit Message Queue)是一个开源、基于AMQP协议的多用途消息队列。它最初起源于金融系统,用于在分布式系统中存储、转发和交换数据。但是随着时间的推移,它已经被越来越多的应用和领域所使用。目前,许多公司都在使用 RabbitMQ 来实现其内部的消息通讯和服务通讯。因此,对 RabbitMQ 的了解也越来越重要。本文将结合作者自己的工作经历,详细阐述 RabbitMQ 中的消息可靠性机制。

2.基本概念术语说明

2.1 消息模型

首先,我们需要对 RabbitMQ 中的消息模型有一个简单的认识。 RabbitMQ 中存在三种类型的实体:生产者、交换机、队列和消费者。

生产者

生产者就是向 RabbitMQ 发送消息的实体。可以由应用程序通过调用 API 将消息投递到队列或交换机上。生产者可以选择不同的选项来决定何时把消息投递到哪个队列或交换机,也可以指定消息的属性如消息的优先级、超时时间、重复次数等。生产者还可以设置多个返回地址,如果消息由于某些原因无法投递到队列或交换机,则会路由到指定的返回地址进行重试。

交换机

RabbitMQ 中的交换机类似于我们现实生活中的交换机。它负责存储接收到的信息并根据一些规则把信息传递给对应的队列。每个队列可以配置绑定到交换机上的路由键。当生产者向交换机发布一条消息时,交换机根据该消息的路由键和绑定的路由键,把消息传递给对应的队列。

队列

队列就是用来存储消息的临时的容器。RabbitMQ 可以创建很多队列,每个队列都可以存储不同类型的消息。比如,可以创建三个不同类型的队列:点播队列、直播队列和私信队列。当然,也可以创建一个混合类型的队列,例如在同一个队列里既存放点播消息又存放直播消息。在这里,我们只讨论消息可靠性相关的内容,因此,这里的队列类型可以简单理解成点播队列、直播队列或私信队列。

消费者

消费者就是从 RabbitMQ 获取消息并处理的实体。当队列中有消息时,消费者就可以获取并处理这些消息。消费者可以设置 prefetch count 属性,表示一次获取多少条消息,这样可以提高性能。同时,也可以设置 QoS 参数,即 Quality of Service,可以控制 RabbitMQ 对消息处理的速度。QoS 可以配置两个参数:prefetch count 和 prefetch size。prefetch count 表示在同一时刻最多可以获取多少条消息;prefetch size 表示每次获取消息的大小限制。

2.2 可靠性

消息可靠性是一个很重要的问题,也是 RabbitMQ 提供的重要功能之一。因为没有可靠的消息传递机制,就算消息被正确的传递到了目标队列或交换机,也不能确保消息一定会被消费者接收到。因此,RabbitMQ 提供了各种可靠性机制来确保消息的传输可靠性。下面介绍一下 RabbitMQ 中主要的可靠性机制。

2.2.1 事务

RabbitMQ 支持事务,也就是一系列动作要么都做,要么都不做。如果事务成功完成,那么所有动作都会被执行;如果事务失败,那么所有的动作都不会被执行。使用事务可以确保在消息传递过程中,要么全部完成,要么全部不做。

//开启事务
channel.txSelect();
try{
    // 发送消息
    channel.basicPublish("","",null,message);
    //提交事务
    boolean commitSuccess = channel.txCommit();
    if(commitSuccess){
        System.out.println("事务提交成功!");
    }else {
        System.out.println("事务提交失败!");
    }
}catch (Exception e){
    try {
        //回滚事务
        boolean rollbackSuccess = channel.txRollback();
        if(rollbackSuccess){
            System.out.println("事务回滚成功!");
        }else {
            System.out.println("事务回滚失败!");
        }
    } catch (IOException ex) {
        ex.printStackTrace();
    }
}finally {
    //关闭连接
    channel.close();
    connection.close();
}

2.2.2 Publisher Confirmations

Publisher Confirmations 是 RabbitMQ 中的一种可靠性机制。它可以让你在确认生产者发送的消息后再确认消费者接收到了消息。这样可以确保消息不丢失,也可以根据需要重试。

boolean isConfirm = true;
if(isConfirm){
    channel.confirmSelect();
}
ListenableFuture<Sent> future = channel.basicPublish("", "", null, message);
if(!isConfirm){
    future.addCallback(new FutureCallback<Sent>() {
        @Override
        public void onFailure(Throwable throwable) {
            // 抛出异常时重试
            doRetry(future);
        }

        @Override
        public void onSuccess(Sent sent) {
            // 确认消息投递成功
        }
    });
}else {
    future.addCallback(new PublishConfirmListener());
}
...
class PublishConfirmListener implements ConfirmListener {
    @Override
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        // 当消息投递到队列并得到ACK时,调用此方法
    }

    @Override
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        // 当消息投递到队列但没得到ACK时,调用此方法
        doRetry(deliveryTag);
    }

    private void doRetry(Object arg) {
        // 重新发送已确认但未确认的消息
       ...
    }

    private void doRetry(long deliveryTag) {
        // 根据deliveryTag重新发送已确认但未确认的消息
       ...
    }
}

2.2.3 持久化

RabbitMQ 支持消息的持久化。这意味着消息在队列中被暂停,然后存储在磁盘上,而不是仅仅保存在内存中。即使消费者突然崩溃了,也不会影响之前发布的消息,因为它们已经被持久化到磁盘上了。持久化消息可以保证消息的可靠传递。

2.2.4 消息持久化跟踪

当消息被持久化时,RabbitMQ 会记录消息元数据,包括消息是否被确认消费,消息是否被持久化等。这样可以帮助你跟踪消息的状态,并知道消息何时可以被安全删除。

2.2.5 死信队列

当 RabbitMQ 由于各种原因(如队列长度过长、消费者处理失败等)而丢弃某些消息时,它可以通过将这些消息重新路由到死信队列来保存。可以设置某些条件来决定是否将消息重新路由到死信队列,如消息的TTL(Time To Live)过期或消息被拒绝(redelivered)。

2.2.6 流量控制

流量控制是指在消息传递过程中,根据消费者处理消息的能力,对消息的数量进行限制。RabbitMQ 通过 prefetch count 设置每秒钟可以接收多少消息,来控制消费者的消息处理速率。

2.2.7 副本队列

为了确保消息的可靠传递,RabbitMQ 支持创建多个队列,称为副本队列。这些队列可以容纳相同的消息,但是只有其中一个副本队列被选举为真正的队列。当发生故障切换时,可以自动地将消息从错误的队列复制到新的队列。这种方式可以减少消息丢失的可能性。

3.核心算法原理及具体操作步骤及数学公式讲解

3.1 多级流控

RabbitMQ 在设计上采用了多级流控策略,来限制消息的流入速率。为了达到较好的性能,RabbitMQ 默认采用优先级队列的方式。优先级队列保证了消息的先后顺序,但是在RabbitMQ的配置项中,可以控制队列的最大消费者数,即限制了每个队列的消费速率。假设每个队列的最大消费者数为 k ,那么理想情况下,第 i 个优先级队列的消费速率应该为 qi = kp,其中 p 为所有优先级队列的总消费者数。RabbitMQ 通过计算当前各个队列的最大消费速率,进一步调整队列的流量。下图展示了 RabbitMQ 的流控策略:

按照默认配置,RabbitMQ 为每个优先级队列分配了一个虚拟节点,每个虚拟节点只能被一个消费者消费,这样可以平衡各个优先级队列之间的流量。对于各个虚拟节点,RabbitMQ 会统计前面所有虚拟节点的平均消费延迟,以便确定当前虚拟节点应获得的权重,这个权重直接影响到虚拟节点的消息处理速率。权重分为三档:1、2、3。权重越高,代表该虚拟节点的消息处理速率越快。每个优先级队列有 k 个虚拟节点,并且每个优先级队列的虚拟节点共享相同的权重。当消费者连接上 RabbitMQ 时,RabbitMQ 会向该消费者分配一个初始权重(默认为1),并且根据历史消息处理延迟情况,动态调整权重。

例如,假设某个队列拥有 10 个虚拟节点,且每秒钟有 50 个消息要消费。如果某个虚拟节点的平均延迟为 0.5s,那么其权重为 5。如果某消费者的平均延迟为 1s,那么其权重为 1。如果某个优先级队列的最大消费者数为 100,那么该队列的消费速率约为 100 × (k/p=5)/0.5 ≈ 100 × 5 / 2 ≈ 2000 msg/s 。

如果消费者的处理能力变差,或者当前队列中消息积压过多,那么它就会被阻塞,直到其他消费者处理完剩余的消息。

另外,RabbitMQ 还提供了两种机制来管理消息的积压。一是 publish rate limit(消息发布速率限制),二是 backpressure(反压)。

publish rate limit 是指允许发布者每秒发布多少消息。这可以在 publisher 配置文件中设置,例如,"x-max-rate": 100 限制每秒钟发布 100 条消息。backpressure 是指 RabbitMQ 不希望消费者在短时间内超载,所以它会停止接受新消息。当消息积压超过特定值时,RabbitMQ 会停止推送消息。

3.2 满足持久化消息要求

RabbitMQ 使用 AMQP 协议来实现消息传递。它通过 TCP/IP 作为传输层协议,并通过 Erlang 编程语言来实现服务器端的逻辑。Erlang 是一个运行在虚拟机中的解释型的函数式编程语言。

为了保证消息持久化,RabbitMQ 将消息保存在磁盘上,并同步到多个磁盘上。它使用消息日志来存储消息。每条消息都有唯一的消息编号,消息日志按顺序存储。RabbitMQ 启动后,会读取磁盘上的消息日志,并根据消息编号来排序。然后,RabbitMQ 从消息日志中读取相应的消息并将其推送到消息队列中。当消费者消费了消息,RabbitMQ 才会将其标记为删除。当有新的消费者加入队列时,RabbitMQ 依然可以读取消息日志中的消息,并按照消息编号排序推送到队列中。

当消费者消费完某条消息后,但是消费者宕机了,这条消息仍然留在消息日志中。RabbitMQ 只是标记该消息为未删除,直到所有消费者都消费完这条消息。然后 RabbitMQ 删除该消息,从磁盘上删除相应的文件。当消息积压很多时,磁盘占用可能会很大。RabbitMQ 提供插件来压缩消息日志,来降低磁盘空间的占用。

猜你喜欢

转载自blog.csdn.net/universsky2015/article/details/133502538