【RabbitMQ】架构原理、消息丢失、重复消费、顺序消费、事务消息

【RabbitMQ】架构原理、消息丢失、重复消费、顺序消费、事务消息

  • 1、RabbitMQ的架构是怎么样的?
    • [1.1 RabbitMQ是怎么做消息分发的?](#1.1 RabbitMQ是怎么做消息分发的?)
    • [1.2 RabbitMQ 是如何保证高可用的?](#1.2 RabbitMQ 是如何保证高可用的?)
  • 2、消息丢失问题
    • [2.1 如何保障消息一定能发送到RabbitMQ?](#2.1 如何保障消息一定能发送到RabbitMQ?)
    • [2.2 RabbitMQ如何保证消息不丢?](#2.2 RabbitMQ如何保证消息不丢?)
  • 3、重复消费问题
  • 4、顺序消费问题
  • 5、事务消息
    • [5.1 介绍下RabbitMQ的事务机制](#5.1 介绍下RabbitMQ的事务机制)
  • 6、延时消息
    • [6.1 什么是RabbitMQ的死信队列?](#6.1 什么是RabbitMQ的死信队列?)
    • [6.2 rabbitMQ如何实现延迟消息?](#6.2 rabbitMQ如何实现延迟消息?)

1、RabbitMQ的架构是怎么样的?

RabbitMQ是一个开源的消息中间件,用于在应用程序之间传递消息。它实现了AMQP(高级消息队列协议)并支持其他消息传递协议,例如STOMP(简单文本定向消息协议)和MQTT(物联网协议)。

整体架构大致如下:

Producer(生产者):生产者是消息的发送方,负责将消息发布到RabbitMQ的交换器(Exchange)。

VHost:是RabbitMQ中虚拟主机的概念,类似于操作系统中的命名空间,用于将RabbitMQ的资源进行隔离和分组。每个VHost拥有自己的交换器、队列、绑定和权限设置,不同VHost之间的资源相互独立,互不干扰。VHost可以用于将不同的应用或服务进行隔离,以防止彼此之间的消息冲突和资源竞争。

Exchange(交换器):交换器是消息的接收和路由中心,它接收来自生产者的消息,并将消息路由到一个或多个与之绑定的队列(Queue)中。

Queue(队列):队列是消息的存储和消费地,它保存着未被消费的消息,等待消费者(Consumer)从队列中获取并处理消息。

Binding(绑定):绑定是交换器和队列之间的关联关系,它定义了交换器将消息路由到哪些队列中。

Consumer(消费者):消费者是消息的接收方,负责从队列中获取消息,并进行处理和消费。

1.1 RabbitMQ是怎么做消息分发的?

rabbitMQ一共有6种工作模式(消息分发方式)分别是简单模式、工作队列模式、发布订阅模式、路由模式、主题模式以及RPC模式。

1、Simple(简单模式)

在简单模式中,一个生产者将消息发送到一个队列中,一个消费者从队列中获取并处理消息。这种模式适用于单个生产者和单个消费者的简单场景,消息的处理是同步的。

2、Work Queue(工作队列模式)

工作队列模式用于实现一个任务在多个消费者之间的并发处理。在工作队列模式中,一个生产者将消息发送到一个队列中,多个消费者从队列中获取并处理消息。每个消息只能被一个消费者处理。这种模式适用于多个消费者并发处理消息的情况,提高了系统的处理能力和吞吐量。

3、Publish/Subscribe(发布/订阅模式)

发布/订阅模式用于实现一条消息被多个消费者同时接收和处理。在发布/订阅模式中,一个生产者将消息发送到交换器(Exchange)中,交换器将消息广播到所有绑定的队列,每个队列对应一个消费者。这种模式适用于消息需要被多个消费者同时接收和处理的广播场景,如日志订阅和事件通知等。

4、Routing(路由模式)

路由模式用于实现根据消息的路由键(Routing Key)将消息路由到不同的队列中。在路由模式中,一个生产者将消息发送到交换器中,并指定消息的路由键,交换器根据路由键将消息路由到与之匹配的队列中。这种模式适用于根据不同的条件将消息发送到不同的队列中,以实现消息的筛选和分发。


5、Topics(主题模式)

主题模式是一种更灵活的消息路由模式,它使用通配符匹配路由键,将消息路由到多个队列中。在主题模式中,一个生产者将消息发送到交换器中,并指定主题(Topic)作为路由键,交换器根据通配符匹配将消息路由到与之匹配的队列中。这种模式适用于消息的复杂路由需求,可以实现高度灵活的消息筛选和分发。

6、RPC通信模式

RPC模式是一种用于实现分布式系统中远程调用的工作模式。指的是通过rabbitMQ来实现一种RPC的能力。

1.2 RabbitMQ 是如何保证高可用的?

RabbitMQ可以通过多种方式来实现高可用性,以确保在硬件故障或其他不可预测的情况下,消息队列系统仍然能够正常运行。RabbitMQ有三种模式:单机模式、普通集群模式、镜像集群模式。

其中单机模式一般用于demo搭建,不适合在生产环境中使用。剩下的集群模式和镜像模式都可以帮助我们实现不同程度的高可用。

1、普通集群模式

普通集群模式,就是将 RabbitMQ 实例部署到多台服务器上,多个实例之间协同工作,共享队列和交换机的元数据,并通过内部通信协议来协调消息的传递和管理。

在这种模式下,我们创建的Queue,它的元数据(配置信息)会在集群中的所有实例间进行同步,但是队列中的消息只会存在于一个 RabbitMQ 实例上,而不会同步到其他队列。

当我们消费消息的时候,如果消费者连接到了未保存消息的实例,那么那个实例会通过元数据定位到消息所在的实例,拉取数据过来发送给消费者进行消费。

消息的发送也是一样的,当发送者连接到了一个不保存消息的实例时,也会被转发到保存消息的实例上进行写操作。

这种集群模式下,每一个实例中的元数据是一样的,大家都是完整的数据。但是队列中的消息数据,在不同的实例上保存的是不一样的。这样通过增加实例的方式就可以提升整个集群的消息存储量,以及性能。

这种方式在高可用上有一定的帮助,不至于一个节点挂了就全都挂了。但是也还有缺点,至少这个实例上的数据是没办法被读写了。

2、镜像模式

顾名思义,就是每一台RabbitMQ都像一个镜像一样,存储的内容都是一样的。这种模式下,Queue的元数据和消息数据不再是单独存储在某个实例上,而是集群中的所有实例上都存储一份。

这样每次在消息写入的时候,就需要在集群中的所有实例上都同步一份,这样即使有一台实例发生故障,剩余的实例也可以正常提供完整的数据和服务。

2、消息丢失问题

2.1 如何保障消息一定能发送到RabbitMQ?

作为消息发送方,如何保证给RabbitMQ发送的消息一定能发送成功,如何确保他一定能收到这个消息?

RabbitMQ的消息最终是存储在Queue上的,而在Queue之前还要经过Exchange,那么这个过程中就有两个地方可能导致消息丢失。第一个是Producer到Exchange的过程,第二个是Exchange到Queue的过程。

上面两个可能丢失的过程,都可以利用confirm机制,注册回调来监听是否成功。

Publisher Confirm是一种机制,用于确保消息已经被Exchange成功接收和处理。一旦消息成功到达Exchange并被处理,RabbitMQ会向消息生产者发送确认信号(ACK)。如果由于某种原因(例如,Exchange不存在或路由键不匹配)消息无法被处理,RabbitMQ会向消息生产者发送否认信号(NACK)。

java 复制代码
// 启用Publisher Confirms
channel.confirmSelect();

// 设置Publisher Confirms回调
channel.addConfirmListener(new ConfirmListener() {
    @Override
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("Message confirmed with deliveryTag: " + deliveryTag);
        // 在这里处理消息确认
    }

    @Override
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("Message not confirmed with deliveryTag: " + deliveryTag);
        // 在这里处理消息未确认
    }
});

Publisher Returns机制与Publisher Confirms类似,但用于处理在消息无法路由到任何队列时的情况。当RabbitMQ在无法路由消息时将消息返回给消息生产者,但是如果能正确路由,则不会返回消息。

java 复制代码
// 启用Publisher Returns
channel.addReturnListener(new ReturnListener() {
    @Override
    public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.out.println("Message returned with replyCode: " + replyCode);
        // 在这里处理消息发送到Queue失败的返回
    }
});

通过以上方式,注册了两个回调监听,用于在消息发送到Exchange或者Queue失败时进行异常处理。通常可以在失败时进行报警或者重试来保障一定能发送成功。

2.2 RabbitMQ如何保证消息不丢?

上面介绍了如何确保RabbitMQ的发送者把消息能够投递给RabbitMQ的Exchange和Queue,那么,Queue又是如何保证消息能不丢的呢?

RabbitMQ在接收到消息后,默认并不会立即进行持久化,而是先把消息暂存在内存中,这时候如果MQ挂了,那么消息就会丢失。所以需要通过持久化机制来保证消息可以被持久化下来。

1、队列和交换机的持久化

在声明队列时,可以通过设置durable参数为true来创建一个持久化队列。持久化队列会在RabbitMQ服务器重启后保留,确保队列的元数据不会丢失。

在声明交换机时,也可以通过设置durable参数为true来创建一个持久化交换机。持久化交换机会在RabbitMQ服务器重启后保留,以确保交换机的元数据不会丢失。

绑定关系通常与队列和交换机相关联。当创建绑定关系时,还是可以设置durable参数为true,以创建一个持久化绑定。持久化绑定关系会在服务器重启后保留,以确保绑定关系不会丢失。

java 复制代码
@Bean
public Queue TestQueue() {
    // 第二个参数durable:是否持久化,默认是false
    return new Queue("queue-name",true,true,false);
}


@Bean
public DirectExchange mainExchange() {
  	//第二个参数durable:是否持久化,默认是false
    return new DirectExchange("main-exchange",true,false);
}

2、持久化消息

生产者发送的消息可以通过设置消息的deliveryMode为2来创建持久化消息。持久化消息在发送到持久化队列后,将在服务器重启后保留,以确保消息不会丢失。

deliveryMode是一项用于设置消息传递模式的属性,用于指定消息的持久性级别:

1(非持久化):这是默认的传递模式。如果消息被设置为非持久化,RabbitMQ将尽力将消息传递给消费者,但不会将其写入磁盘,这意味着如果RabbitMQ服务器在消息传递之前崩溃或重启,消息可能会丢失。

2(持久化):如果消息被设置为持久化,RabbitMQ会将消息写入磁盘,以确保即使在RabbitMQ服务器重启时,消息也不会丢失。持久化消息对于重要的消息非常有用,以确保它们不会在传递过程中丢失。

3、消费者确认机制

有了持久化机制后,那么怎么保证消息在持久化下来之后一定能被消费者消费呢?这里就涉及到消息的消费确认机制。

在RabbitMQ中,消费者处理消息成功后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息,这样才能确保消息不会丢失。如果消费者在处理消息中出现了异常,那么就会返回nack回执,MQ收到回执之后就会重新投递一次消息,如果消费者一直都没有返回ACK/NACK的话,那么他也会在尝试重新投递。

4、无法做到100%不丢

虽然我们通过发送者端进行异步回调、MQ进行持久化、消费者做确认机制,但是也没办法保证100%不丢,因为MQ的持久化过程其实是异步的。即使我们开了持久化,也有可能在内存暂存成功后,异步持久化之前宕机了,那么这个消息就会丢失

如果想要做到100%不丢失,就需要引入本地消息表,来通过轮询的方式来进行消息重投。

3、重复消费问题

RabbitMQ的消息消费是有确认机制的,正常情况下,消费者在消费消息消费成功后,会发送一个确认消息,消息队列接收到之后,就会将该消息从消息队列中删除,下次也就不会再投递了。

但是如果存在网络延迟的问题,导致确认消息没有发送到消息队列,导致消息重投了,是有可能的。所以,当我们使用MQ的时候,消费者端自己也需要做好幂等控制来防止消息被重复消费。

一般来说,处理这种幂等问题就是:一锁、二判、三更新。

4、顺序消费问题

根据路由键将消息路由到指定队列,队列对应多个消费者。通过 basicQos(1) 保证每个消费者一次只处理一条消息,做到顺序消费的同时,保证消费能力。

5、事务消息

5.1 介绍下RabbitMQ的事务机制

想要保证发送者一定能把消息发送给RabbitMQ,一种是通过confirm机制,另外一种就是通过事务机制。

RabbitMQ的事务机制,允许生产者将一组操作打包成一个原子事务单元,要么全部执行成功,要么全部失败。事务提供了一种确保消息完整性的方法,但需要谨慎使用,因为它们对性能有一定的影响。

因为事务机制是同步的,提交一个事务之后会阻塞在那儿,但是 confirm机制是异步的,发送一个消息之后就可以发送下一个消息,RabbitMQ 接收了之后会异步回调confirm接口通知这个消息接收到了。一般在生产者这块避免数据丢失,建议使用用 confirm 机制

RabbitMQ是基于AMQP协议实现的,RabbitMQ中,事务是通过在通道(Channel)上启用的,与事务机制有关的方法有三个:

● txSelect():将当前channel设置成transaction模式。

● txCommit():提交事务。

● txRollback():回滚事务。

我们需要先通过txSelect开启事务,然后就可以发布消息给MQ了,如果txCommit提交成功了,则消息一定到达了RabbitMQ,如果在txCommit执行之前RabbitMQ实例异常崩溃或者抛出异常,那我们就可以捕获这个异常然后执行txRollback进行回滚事务。

所以, 通过事务机制,我们也能保证消息一定可以发送给RabbitMQ。

以下,是一个通过事务发送消息的方法示例:

java 复制代码
import com.rabbitmq.client.*;

public class RabbitMQTransactionExample {

    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.txSelect();

            String exchangeName = "my_exchange";
            String routingKey = "my_routing_key";

            try {
                // 发送第一条消息
                String message1 = "Transaction Message 1";
                channel.basicPublish(exchangeName, routingKey, null, message1.getBytes());

                // 发送第二条消息
                String message2 = "Transaction Message 2";
                channel.basicPublish(exchangeName, routingKey, null, message2.getBytes());

                // 模拟一个错误
                int x = 1 / 0;

                // 提交事务(如果没有发生错误)
                channel.txCommit();

                System.out.println("Transaction committed.");
            } catch (Exception e) {
                // 发生错误,回滚事务
                channel.txRollback();
                System.err.println("Transaction rolled back.");
            }
        }
    }
}

6、延时消息

6.1 什么是RabbitMQ的死信队列?

RabbitMQ的死信队列(Dead Letter Queue,简称DLQ)是一种用于处理消息处理失败或无法路由的消息的机制。它允许将无法被正常消费的消息重新路由到另一个队列,以便稍后进行进一步的处理、分析或排查问题。

当消息队列里面的消息出现以下几种情况时,就可能会被称为"死信":

  1. 消息处理失败:当消费者由于代码错误、消息格式不正确、业务规则冲突等原因无法成功处理一条消息时,这条消息可以被标记为死信。
  2. 消息过期:在RabbitMQ中,消息可以设置过期时间。如果消息在规定的时间内没有被消费,它可以被认为是死信并被发送到死信队列。
  3. 消息被拒绝:当消费者明确拒绝一条消息时,它可以被标记为死信并发送到死信队列。拒绝消息的原因可能是消息无法处理,或者消费者认为消息不符合处理条件。
  4. 消息无法路由:当消息不能被路由到任何队列时,例如,没有匹配的绑定关系或路由键时,消息可以被发送到死信队列。

当消息变成"死信"之后,如果配置了死信队列,它将被发送到死信交换机,死信交换机将死信投递到一个队列上,这个队列就是死信队列。但是如果没有配置死信队列,那么这个消息将被丢弃。

RabbitMQ的死信队列其实有很多作用,比如我们可以借助他实现延迟消息,进而实现订单的到期关闭,超时关单等业务逻辑。

6.2 rabbitMQ如何实现延迟消息?

RabbitMQ中是可以实现延迟消息的,一般有两种方式,分别是通过死信队列以及通过延迟消息插件来实现。

1、死信队列

当RabbitMQ中的一条正常的消息,因为过了存活时间(TTL过期)、队列长度超限、被消费者拒绝等原因无法被消费时,就会变成Dead Message,即死信。

当一个消息变成死信之后,他就能被重新发送到死信队列中(其实是交换机-exchange)。

那么基于这样的机制,就可以实现延迟消息了。那就是我们给一个消息设定TTL,但是并不消费这个消息,等他过期,过期后就会进入到死信队列,然后我们再监听死信队列的消息消费就行了。

而且,RabbitMQ中的这个TTL是可以设置任意时长的,这相比于RocketMQ只支持一些固定的时长而显得更加灵活一些

但是,死信队列的实现方式存在一个问题,那就是可能造成队头阻塞。RabbitMQ会定期扫描队列的头部,检查队首的消息是否过期。如果队首消息过期了,它会被放到死信队列中。然而,RabbitMQ不会逐个检查队列中的所有消息是否过期,而是仅检查队首消息。这样,如果队列的队头消息未过期,而它后面的消息已过期,这些后续消息将无法被单独移除,直到队头的消息被消费或过期。

因为队列是先进先出的,在普通队列中的消息,每次只会判断队头的消息是否过期,那么,如果队头的消息时间很长,一直都不过期,那么就会阻塞整个队列,这时候即使排在他后面的消息过期了,那么也会被一直阻塞。

基于RabbitMQ的死信队列,可以实现延迟消息,非常灵活的实现定时关单,并且借助RabbitMQ的集群扩展性,可以实现高可用,以及处理大并发量。他的缺点第一是可能存在消息阻塞的问题,还有就是方案比较复杂,不仅要依赖RabbitMQ,而且还需要声明很多队列出来,增加系统的复杂度。

2、RabbitMQ插件

其实,基于RabbitMQ的话,可以不用死信队列也能实现延迟消息,那就是基于rabbitmq_delayed_message_exchange插件,这种方案能够解决通过死信队列实现延迟消息出现的消息阻塞问题。但是该插件从RabbitMQ的3.6.12开始支持的,所以对版本有要求。

这个插件是官方出的,可以放心使用,安装并启用这个插件之后,就可以创建x-delayed-message类型的交换机了。

基于死信队列的方式,是消息先会投递到一个正常队列,在TTL过期后进入死信队列。但是基于插件的这种方式,消息并不会立即进入队列,而是先把他们保存在一个基于Erlang开发的Mnesia数据库中,然后通过一个定时器去查询需要被投递的消息,再把他们投递到x-delayed-message交换机中。

基于RabbitMQ插件的方式可以实现延迟消息,并且不存在消息阻塞的问题,但是因为是基于插件的,而这个插件支持的最大延长时间是(2^32)-1 毫秒,大约49天,超过这个时间就会被立即消费。

不过这个方案也有一定的限制,它将延迟消息存在于 Mnesia 表中,并且在当前节点上具有单个磁盘副本,存在丢失的可能

目前该插件的当前设计并不真正适合包含大量延迟消息(例如数十万或数百万)的场景,详情参见 #/issues/72 另外该插件的一个可变性来源是依赖于 Erlang 计时器,在系统中使用了一定数量的长时间计时器之后,它们开始争用调度程序资源,并且时间漂移不断累积。

参考链接:

1、https://www.yuque.com/hollis666/wk6won/qh56y0u8fs2gom42

2、https://www.yuque.com/hollis666/wk6won/lllwvk

3、https://www.yuque.com/hollis666/wk6won/inmrfqk0qyvsdgg3

4、https://www.yuque.com/hollis666/wk6won/ku3fxiie005axgrz

相关推荐
●VON24 分钟前
深入昇腾NPU:从架构到算子开发的全栈探索
架构·昇腾·昇腾npu·gpt-oss-20b·昇腾训练营
一叶飘零_sweeeet1 小时前
幂等性 VS 分布式锁:分布式系统一致性的两大护法 —— 从原理到实战的深度剖析
分布式·分布式锁·接口幂等
更深兼春远1 小时前
Spark on Yarn安装部署
大数据·分布式·spark
Mxsoft6192 小时前
电力设备绝缘状态分布式光纤传感实时监测与多维度诊断技术
分布式
Java爱好狂.2 小时前
分布式ID|从源码角度深度解析美团Leaf双Buffer优化方案
java·数据库·分布式·分布式id·es·java面试·java程序员
Wang's Blog3 小时前
Nestjs框架: 微服务项目工程结构优化与构建方案
微服务·云原生·架构·nestjs
GM_8283 小时前
初识DDD架构
架构
极造数字4 小时前
从EMS看分布式能源发展:挑战与机遇并存
人工智能·分布式·物联网·信息可视化·能源·制造
wangruofeng5 小时前
为 CI/CD 装上“眼睛”:App 包大小监控的实践
ci/cd·架构
阿萨德528号7 小时前
Redis 分布式锁进阶:跨语言场景下的锁兼容性与一致性保障
数据库·redis·分布式