【个人笔记】消息队列 - RabbitMQ

文章目录

  • 1. MQ 的作用
    • 1.1 流量消峰
    • 1.2 应用解耦
    • 1.3 异步处理
  • 2. 四大核心概念
    • 名词介绍
    • 2.1 生产者
    • 2.2 消费者
    • 2.3 交换机
    • 2.4 队列
  • 3.安装
  • 4.创建 java 开发环境
    • 4.1 导入依赖
    • 4.2 生产者消费者 demo
  • 5. 工作队列原理
    • 5.1 轮询分发消息
    • 5.2 消息应答
      • 5.2.1 自动应答
      • 5.2.2 手动应答
        • 5.2.2.1 Multiple 的解释
        • 5.2.2.2 消息自动重新入队
  • 6. RabbitMQ 持久化
    • 6.1 队列持久化
    • 6.2 消息实现持久化
  • 7. 不公平分发/预取值
  • 8. 发布确认
    • 8.2 发布确认策略
      • 8.2.1 单个确认发布
      • 8.2.2 批量发布确认
      • 8.2.3 异步发布确认
        • 8.2.3.1 如何处理异步未确认消息
  • 9. 交换机
    • 9.1 交换机的类型
      • 9.1.1 无名
      • 9.1.2 扇出(fanout)
      • 9.1.3 直接(direct)
      • 9.1.4 主题(topic)
  • 10.临时队列
  • 11.绑定
  • 12.死信队列
  • 13. 延迟队列
    • 13.1 RabbitMQ 中的 TTL
      • 13.1.1 消息设置TTL
      • 13.1.2 队列设置TTL
  • 相关面试题
    • 1. RabbitMQ 事务消息机制
    • 2. RabbitMQ 普通集群模式
    • 3. RabbitMQ 架构设计
    • 4.RabbitMQ 交换机类型
    • 5.RabbitMQ 持久化机制
    • 6.RabbitMQ 死信队列、延迟队列
    • 7.RabbitMQ 如何保证消息可靠传输
    • 8.RabbitMQ 可以直连队列吗
    • 9.RabbitMQ 镜像队列原理
    • 10.RabbitMQ 怎么实现顺序消息
    • 11.消费者生产者有没有连接数限制
    • 12.AMQP 协议
    • 13.顺序消费、重复消费、消息积压

1. MQ 的作用

1.1 流量消峰

举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,
如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。

1.2 应用解耦

以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。

1.3 异步处理

有些服务间调用是异步的,例如A调用B,B需要花费很长时间执行,但是A需要知道B什么时候可以执行完,以前一般有两种方式,A过一段时间去调用B的查询api查询。或者A提供一个callback api,B执行完之后调用api通知A服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A调用B服务后,只需要监听B处理完成的消息,当B处理完成后,会发送一条消息给MQ,MQ会将此消息转发给A服务。这样A服务既不用循环调用B的查询api,也不用提供callback api。同样B服务也不用做这些操作。A服务还能及时的得到异步处理成功的消息。

2. 四大核心概念

【个人笔记】消息队列 - RabbitMQ_第1张图片

名词介绍

【个人笔记】消息队列 - RabbitMQ_第2张图片
Broker:接收和分发消息的应用,RabbitMQ Server (RabbitMQ 的服务器)就是 Message Broker。
Virtual host :出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost ,每个用户在自己的 vhost创建 exchange/queue等。
Connection:publisher/consumer 和 broker 之间的 TCP 连接
Channel :如果每一次访问 RabbitMQ 都建立一个 Connection ,在消息量大的时候建立TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel
进行通讯, AMQP method包含了channel id 帮助客户端和 message broker识别
channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销

2.1 生产者

产生数据发送消息的程序是生产者

2.2 消费者

消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。

2.3 交换机

交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定

2.4 队列

队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式。

3.安装

我用的 docker ,安装命令如下:

docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management

启动页面:

http://127.0.0.1:15672
用户名密码都是: guest

4.创建 java 开发环境

4.1 导入依赖

 <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <configuration>
                    <source>8source>
                    <target>8target>
                configuration>
            plugin>
        plugins>
    build>

    <dependencies>
        <dependency>
            <groupId>com.rabbitmqgroupId>
            <artifactId>amqp-clientartifactId>
            <version>5.8.0version>
        dependency>
        <dependency>
            <groupId>commons-iogroupId>
            <artifactId>commons-ioartifactId>
            <version>2.6version>
        dependency>
    dependencies>

4.2 生产者消费者 demo

代码实现对应上面的原理图,连接工厂创建出多个连接,一个连接里面是多个信道,然后信道连接到交换机(这里的代码没有体现连接交换机),交换机接收消息,然后推送到队列中

tip:这里实际上是连接上了一个默认的交换机,这里的交换机将队列名称作为 routingKey 连接到队列

生产者:

public class Producer {
    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("guest");
        factory.setPassword("guest");
        Connection connection = factory.newConnection();
        //信道
        Channel channel = connection.createChannel();
        /**
         * 声明一个队列
         * 1、队列名称
         * 2、服务器重启后队列是否还存在
         * 3、true 表示只能有一个消费者消费,false 表示可以有多个
         * 4、最后一个消费者断开连接后,该队列是否自动删除,true 自动删除,false 不自动删除
         * 5、其他参数
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        String message = "hello word";
        /**
         * 发送消息
         * 1、发送到哪个交换机
         * 2、路由的 key 值是哪一个,本次是队列名称
         * 3、其他参数信息
         * 4、发送消息的消息体
         */
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
        System.out.println("消息发送成功");
    }
}

结果展示:
【个人笔记】消息队列 - RabbitMQ_第3张图片
消费者:

public class Consumer {
    /**
     * 和生产者的队列要一样
     */
    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("guest");
        factory.setPassword("guest");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        //消费成功的回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println(new String(message.getBody()));
        };

        //消息消费失败的回调
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消息消费被中断");
        };

        /**
         * 消费者消费消息
         * 1、消费那个队列
         * 2、消费成功之后是否自动应答
         * 3、消费者成功消费的回调
         * 4、消费者取消/失败消费的回调
         */
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
    }
}

结果展示:
【个人笔记】消息队列 - RabbitMQ_第4张图片

5. 工作队列原理

工作队列主要解决的问题:处理资源密集型任务,并且还要等他完成。有了工作队列,我们就可以将具体的工作放到后面去做,将工作封装为一个消息,发送到队列中,在后台运行的工作线程将弹出任务并最终执行。如果启动了多个工作线程,那么这些工作线程将一起处理这些任务

5.1 轮询分发消息

公平的依次发给每一个消费者,每个消费者消费1个

5.2 消息应答

为了确保消息不会丢失,RabbitMQ支持消息应答。消费者发送一个消息应答,告诉RabbitMQ这个消息已经接收并且处理完毕了。RabbitMQ就可以删除它了。

如果一个消费者挂掉却没有发送应答,RabbitMQ会理解为这个消息没有处理完全,然后交给另一个消费者去重新处理。这样,即使消费者偶尔挂掉也不会丢失任何消息了。

5.2.1 自动应答

默认情况下,rabbitmq开启了消息的自动应答。此时,一旦rabbitmq将消息分发给了消费者,就会将消息从内存中删除。这种情况下,如果正在执行的消费者被“杀死”或“崩溃”,就会丢失正在处理的消息。

5.2.2 手动应答

消费者处理完业务逻辑,手动返回ack(通知)告诉队列处理完了,队列进而删除消息。

三种方式:

A. Channel.basicAck (用于肯定确认)
RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了

B. Channel.basicNack (用于否定确认)

C.Channel.basicReject (用于否定确认)
与 Channel.basicNack 相比少一个参数(批量处理:Multiple) 不处理该消息了直接拒绝,可以将其丢弃了

5.2.2.1 Multiple 的解释

手动应答的好处是可以批量应答并且减少网络拥堵

【个人笔记】消息队列 - RabbitMQ_第5张图片
true 代表批量应答 channel 上未应答的消息
比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8 那么此时
5-8 的这些还未应答的消息都会被确认收到消息应答
false 同上面相比
只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答

【个人笔记】消息队列 - RabbitMQ_第6张图片

5.2.2.2 消息自动重新入队

如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息 未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者 可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确 保不会丢失任何消息。【所以消息在手动应答的时候是不会丢失的】

6. RabbitMQ 持久化

如何保障当 RabbitMQ 服务停掉以后消 息生产者发送过来的消息不丢失。

6.1 队列持久化

之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果 要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化在这里插入图片描述
需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现错误

6.2 消息实现持久化

要想让消息实现持久化需要在消息 生产者 修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添加这个属性。
在这里插入图片描述

将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候,但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘,这时候宕机消息就会丢失。但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要更强有力的持久化策略,参考后边发布确认的内容。

7. 不公平分发/预取值

在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2 处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间 处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是 RabbitMQ 并不知道这种情况它依然很公平的进行分发。
为了避免这种情况,我们可以在消费者代码中设置参数 channel.basicQos(prefetchCount);
这句话的意思就是,每次分配给该消费者的任务数量不能超过 prefetchCount 条。

参数 prefetchCount 叫作预取值,该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量, RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认。所以默认为 0 的时候就是轮询分发,大于 0 的时候就是不公平分发

【个人笔记】消息队列 - RabbitMQ_第7张图片

8. 发布确认

生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消 息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会 发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了, 如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产 者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道 返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方 法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息, 生产者应用程序同样可以在回调方法中处理该 nack 消息。

8.2 发布确认策略

发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布 确认,都需要在 channel 上调用该方法
在这里插入图片描述

8.2.1 单个确认发布

这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它 被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认 的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会 阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某 些应用程序来说这可能已经足够了。

8.2.2 批量发布确认

上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地 提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现 问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种 方案仍然是同步的,也一样阻塞消息的发布。

8.2.3 异步发布确认

异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说, 他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功, 下面就让我们来详细讲解异步确认是怎么实现的。

8.2.3.1 如何处理异步未确认消息

最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列, 比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。

tip : 综上,想要消息不丢失需要三个条件
1、队列持久化
2、消息持久化
3、发布确认

9. 交换机

RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产 者甚至都不知道这些消息传递传递到了哪些队列中。
相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来 自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消 息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。

9.1 交换机的类型

总共有以下类型:
直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)【扇出也叫发布订阅】,无名

9.1.1 无名

无名交换机类型:
在本教程的前面部分我们对 exchange 一无所知,但仍然能够将消息发送到队列。之前能实现的 原因是因为我们使用的是默认交换,我们通过空字符串(“”)进行标识。
在这里插入图片描述
第一个参数是交换机的名称。空字符串表示默认或无名称交换机:消息能路由发送到队列中其实是由 routingKey(bindingkey)绑定 key 指定的,如果没有指定这个 key ,默认就是队列名称【上面那个 “hello”】。

9.1.2 扇出(fanout)

Fanout 这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到与该交换机绑定的所有队列中,也就是 routingKey 不同的,也能被广播到。

9.1.3 直接(direct)

Fanout 这种交换类型并不能给我们带来很大的灵活性,它只能进行无意识的广播,在这里我们将使用 direct 这种类型来进行替换,这种类型的工作方式是,消息只去到它绑定的 routingKey 队列中去。
【个人笔记】消息队列 - RabbitMQ_第8张图片
在上面这张图中,我们可以看到 X 绑定了两个队列,绑定类型是 direct。队列Q1 绑定键为 orange,队列 Q2 绑定键有两个:一个绑定键为 black,另一个绑定键为 green.
在这种绑定情况下,生产者发布消息到 exchange 上,绑定键为 orange 的消息会被发布到队列 Q1。绑定键为 black 和 green 的消息会被发布到队列 Q2,其他消息类型的消息将被丢弃

9.1.4 主题(topic)

尽管使用direct 交换机改进了我们的系统,但是它仍然存在局限性,比方说我们想接收的日志类型有 info.base 和 info.advantage,某个队列只想要 info.base 的消息,那这个时候direct 就办不到了。这个时候就只能使用 topic 类型

*(星号)可以代替一个单词
#(井号)可以替代零个或多个单词

tip: 发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。最多不能超过 255 个字节。

案例演示:
【个人笔记】消息队列 - RabbitMQ_第9张图片
【个人笔记】消息队列 - RabbitMQ_第10张图片

10.临时队列

之前的章节我们使用的是具有特定名称的队列(还记得 hello 和 ack_queue 吗?)。队列的名称我们来说至关重要。我们需要指定我们的消费者去消费哪个队列的消息。
每当我们连接到 Rabbit 时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。
创建临时队列的方式如下:【就是不指定队列名称,创建出来的就是临时队列】
在这里插入图片描述

11.绑定

bingding 呢,binding 其实是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和那个队列进行了绑定关系。 exchange 和 queue 之间通过 routineKey 进行绑定

12.死信队列

先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理 解,一般来说,producer 将消息投递到 broker 或者直接到queue 里了,consumer 从 queue 取出消息 进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有 后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息 消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时 间未支付时自动失效

产生原因:

  • 消息 TTL 过期
  • 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
  • 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false(不放回队列中)

13. 延迟队列

延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望 在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的 元素的队列。

13.1 RabbitMQ 中的 TTL

TTL 是什么呢?TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有 消息的最大存活时间,单位是毫秒。换句话说,如果一条消息设置了 TTL 属性或者进入了设置TTL 属性的队列,那么这 条消息如果在TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的TTL 和消息的 TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。

tip: 如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡“,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列, 如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

13.1.1 消息设置TTL

在这里插入图片描述

13.1.2 队列设置TTL

在这里插入图片描述

相关面试题

1. RabbitMQ 事务消息机制

从生产者角度来说,通过对事务队列的信道进行设置:

  1. channel.txSelect() 声明启动事务模式
  2. channel.basicPublish() 发送消息,可以是多条,可以是消费消息提交 ack
  3. channel.txCommit() 提交事务
  4. channel.txRollback()回滚事务

从消费者方的角度来说:
1、可以设置成手动确认的模式,即设置 autoAck = false , 信道设置开启事务,手动提交 ack ,提交事务。( autoAck = true 的时候是不能支持事务的,因为自动确认模式下消费从生产者发送出去的时候就顺带被删了)

2. RabbitMQ 普通集群模式

RabbitMQ 集群就两种模式,一种是普通集群模式,一种是镜像队列模式,普通集群模式并不能解决高可用
【个人笔记】消息队列 - RabbitMQ_第11张图片
元数据包括:

  • 队列元数据:队列名称和它的属性
  • 交换器元数据:交换器名称、类型和属性
  • 绑定元数据:一张简单的表格展示了如何将消息路由到队列
  • vhost 元数据:为 vhost 内的队列、交换器和绑定提供命名空间和安全属性。可以理解就是一个 broker,区别就在于 broker 是物理主机,vhost 是虚拟主机,一个物理主机可以建立多个虚拟主机。

为什么只同步元数据:

  • 存储空间:每一个节点都保存全量数据,影响处理消息堆积的能力,因为不管你增加多少个节点,多余的消息还是没地方放
  • 性能,消息的发布者需要将消息复制到每一个集群节点

集群节点的类型:

  • 磁盘节点:将配置信息和元信息存储在磁盘上
  • 内存节点:将配置信息存储在内存中。性能优于磁盘节点。依赖磁盘节点进行持久化
RabbitMQ 要求集群中至少有一个磁盘节点,当节点加入和离开集群时,必须通知磁盘节点(如果磁盘中唯一的磁盘节点崩溃了,则不能进行创建队列、创建交换器、创建绑定、添加用户、更改权限、添加和删除集群节点)。如果唯一磁盘的磁盘节点崩溃,集群是可以保持运行的,但不能更改任何东西。因此建议在集群中设置两个磁盘节点

tip: 看上面的原理图,消息的内容只会在一个节点,元数据是每个节点都会被复制,同步到每个节点上的,这也是普通集群模式为什么做不了高可用的原因,一个节点宕机了,那个节点上的队列就不能再写入消息,那个队列里面的消息也没有地方可以读到了

3. RabbitMQ 架构设计

【个人笔记】消息队列 - RabbitMQ_第12张图片
broker: rabbitMQ 的服务节点

queue: 队列,是 rabbitMQ 的内部对象,用于存储消息。rabbitMQ 中消息只能存储在队列中。生产者投递消息到队列,消费者从队列中获取消息并消费。多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(默认是轮询模式,如果有其他设置就不是这样)给多个消费者进行消费,而不是每个消费者都收到消息进行消费。(注意:rabbitMQ 不支持队列层面的广播消费,如果需要广播消费,可以采用一个交换器通过路由 key 绑定多个队列,由多个消费者来订阅这些队列的方式)

exchange: 交换器。生产者将消息发送到 Exchange ,由交换器将消息路由到一个或多个队列中。如果路由不到,或返回给生产者,或直接丢弃,或做其他处理

routingKey: 路由 key。生产者将消息发送给交换器的时候,一般会指定一个 routingKey,用来指定这个消息的路由规则。这个路由 key 需要预交换器类型和绑定键(bindingKey)联合使用才能生效,在交换器类型和绑定建固定的情况下,生产者可以在发送消息给交换器的时候通过指定 routingKey 来决定消息流向哪里

bindingKey:通过绑定,将交换器和队列关联起来,在绑定的时候一般会指定一个绑定键,这样 rabbitMQ 就可以指定如何正确到路由队列了。交换器和队列实际上是多对多的关系。

信道:信道是建立在 connection 之上的虚拟连接,当应用程序与 rabbitMQ 建立 TCP 连接的时候,客户端紧接着可以创建一个 AMQP 信道(channel),每个信道都会指派一个唯一的 id。rabbitMQ 处理的每条 AMQP 指令都

tip: 不同的 MQ 消费有两种不同的模式: pull 模式或者 push 模式,pull 就是自己去队列里面取消息, push 就是队列下发下来,RabbitMQ 这两种模式都支持。

设置的语法:
pull 模式: channel.basicGet
push 模式: channel.basicConsume

4.RabbitMQ 交换机类型

交换器分发会先找出绑定队列,然后再判断 routeKey ,来决定是否将消息分发到某个队列中
【个人笔记】消息队列 - RabbitMQ_第13张图片
fanout:扇形交换机,不再判断 routeKey ,直接将消息分发到所有绑定队列

direct:判断 routeKey 的规则是完全匹配模式,即发送消息时指定的 roteKey 要等于绑定的 routeKey 。默认交换机使用的 direct 类型

topic:判断 routeKey 的规则是模糊匹配模式

header:绑定队列与交换器的时候指定一个键值对,当交换器在分发消息的时候会先解开消息体里的 headers 数据,然后判断里面是否有所这只的键值对,如果发现陪陪成功,才将消息分发到队列中,这种交换器类型再性能上相对来说比较差,在实际工作中很少会用到

tip: 不管是在消费者还是生产者创建的队列或者交换器,最后都是在 broker 维护的,所以在哪创建没太大区别,只是你在哪创建的,创建时候的参数就得在哪维护

5.RabbitMQ 持久化机制

交换机持久化: exchange_declare 创建交互机时通过参数指定
队列持久化:queue_declare 创建队列时通过参数指定
消息持久化:new AMQPMessage 创建消息时通过参数指定

持久化的原理:
将内容以 append 的方式写文件,会根据大小自动生成新的文件,rabbitMQ 启动时会创建两个进程,一个负责持久化消息的存储,另一个负责非持久化消息的存储(内存不够的时候才会把非持久化的消息写入磁盘)

消息存储时会在 ets 表中记录消息在文件中的映射以及相关信息(包括 id、偏移量、有效数据、左边文件、右边文件),消息读取时根据该信息到文件中读取、同时更新信息

消息删除时只从 ets 删除,变为垃圾数据,当垃圾数据超出比例(默认 50%),并且文件数达到 3 个,触发垃圾回收,锁定文件,从最左边开始依次整理左边文件的有效数据,将右边的文件的有效数据依次写入左边,当一个文件的有用数据等于0时,就删除该文件

怎么保证持久化:
写入文件前先写 buffer 缓冲区,如果 buffer 已满,则写入文件(此时只是操作系统的页存)
每隔 25ms 刷一次磁盘,不管 buffer 满没满,都将 buffer 和页存中的数据落盘
每次消息写入后,如果没有后续写入请求,则直接刷盘

6.RabbitMQ 死信队列、延迟队列

哪些消息会被放入死信队列:

  • 消息被消费方否定确认,使用 channel.basicNackchannel.basicReject,并且此时 requeue 属性被设置成 false
  • 消息在队列中的存活时间超过设置的 TTL 时间
  • 消息队列的消息数量超过最长队列长度

“死信” 消息会被 rabbitMQ 进行特殊处理,如果配置了死信队列信息,那么改消息会被丢进死信队列中,如果没有配置,该消息将被丢弃

为每个需要使用的死信业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由 key,死信队列只不过是绑定在死信交换机上的队列,死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,可以为任意类型【direct、fanout、topic】

延迟队列实现原理:
TTL:一条消息或者该队列中的所有消息的最大存活时间

延迟队列其实就是死信队列的一种应用场景,当一条消息设置了 TTL 属性或者进入了设置了 TTL 属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为“死信”,如果同时设置了队列的 TTL 和 消息的 TTL,那么较小的那个值会被使用。消费者去消费死信队列里消息,就相当于去消费超过 TTL 时间的消息,也就实现了消息的延迟消费

7.RabbitMQ 如何保证消息可靠传输

  • 使用事务消息
  • 使用消息确认机制
    • 发送方确认
      1. channel 设置为 confirm 模式,则每条消息会被分配一个唯一 id
      2. 消息投递成功,信道会发送 ack 给生产者,包含了 id,回调 ConfirmCallback 接口
      3. 如果发送错误导致消息丢失,发送 nack 给生产者。回调 ReturnCallBack 接口
      4. ack 和 nack 只有一个触发,且只有一次,异步触发
    • 接收方确认
      • 声明队列时,指定 noack = false, broker 会等待消费者手动返回 ack,否则会立即删除
      • broker 的 ack 没有超时机制,只会判断连接是否断开,如果断开,消息会被重新发送(这里注意消息幂等性的问题)

8.RabbitMQ 可以直连队列吗

我理解是不会直连队列的,当不声明交换机的时候,会有一个默认的交换机,对应上面交换机类型的 无名

9.RabbitMQ 镜像队列原理

【个人笔记】消息队列 - RabbitMQ_第14张图片
tip: slave 默认是不会对外提供服务的,只负责同步数据,master 负责数据的读写

GM 负责消息的广播,所有的 GM 组成 gm_group ,形成链表结构,负责监听相邻节点的状态,以及传递消息到相邻节点,master 的 GM 收到消息时代表消息同步完成

mirror_queue_master/slave 负责消息的处理,操作 blockingQueue(真正存放消息的队列),Queue 负责 AMQP 协议(commit、rollback、ack 等)

当 master 宕机的时候,就会从 slave 中选一个作为新的 master ,这是高可用的原理

10.RabbitMQ 怎么实现顺序消息

消息队列默认是不能保证顺序的,需要程序保证发送和消费的是同一个 queue。多线程也不能保证顺序

保证发送顺序:发送端子机业务逻辑保证先后,发往一个固定的 queue,生产者可以在消息体上设置消息的顺序
发送者实现 MessageQueueSelector 接口,选择一个 queue 进行发送,也可使用 rocketMQ 提供的默认实现

  • SelectMessageQueueByHash:按参数的 hashcode 与可选队列进行求余选择
  • SelectMessageQueueByRandom:随机选择

mq: queue 本身就是顺序追加写,只需要保证一个队列同一时间只有一个 consumer 消费,通过加锁实现,consumer 上的顺序消费有一个定时任务,每隔一定时间向 broker 发送请求延长锁定

消费端保证顺序:
pull 模式:消费者需要自己维护需要拉取的 Queue,一次拉取的消息都是顺序的,需要消费端自己保证顺序消费

push 模式:消费实例实现自 MQPushConsumer 接口,提供注册监听方法消费消息, registerMessageListerner 、重载方法

  • MessageListernerConcurrently:并行消费
  • MessageListernerOrderly:串行消费, consumer 会把消息放入本地队列并加锁,定时任务保证锁同步

11.消费者生产者有没有连接数限制

12.AMQP 协议

13.顺序消费、重复消费、消息积压

顺序消费:队列本身有序,一个队列对应一个消费者就是有序的。不要用多个消费者
重复消费:消费接口幂等性设计,保证一次消费个多次结果一致
消息积压:消息积压产生的原因主要是生产速度大于消费速度,可以多部署几个消费者,或者生产者和消费者分离,使用两套 mq,中间使用延迟转发

你可能感兴趣的:(rabbitmq,分布式,java)