中间件知识点-消息中间件(Rabbitmq)一

消息中间件介绍

MQ的作用(优点)主要有以下三个方面:

a.异步

b.解耦

c.削峰

MQ的作用(缺点)主要有以下三个方面:

a.系统可用性降低

b.系统复杂度提高

c.存在消息一致性问题需要解决

备注:

引入MQ后系统的复杂度会大大提高。

以前服务之间可以进行同步的服务调用,引入MQ后,会变为异步调用,数据的链路就会变得更复杂。并且还会带来其他一些问

题。比如:如何保证消费不会丢失?不会被重复调用?怎么保证消息的顺序性等问题。(消息一致性问题)

A系统处理完业务,通过MQ发送消息给B、C系统进行后续的业务处理。如果B系统处理成功,C系统处理失败怎么办?这就需要考虑如何保证消息数据处理的一致性。

几大MQ产品特点比较(简单介绍)

rabbitmq可靠性:即不会丢数据,每个节点都存着全量数据(即数据一致性,rabbitmq仲裁队列quorum基于raft协议保证数据一致性,保证多半节点确认后才将消息写到队列中)。

kafka高可用:即使用了分区副本,把数据分散,保证高可用,不会因为一台机器宕机就无法使用。

kafka吞吐量大:使用了分区后可以在多台机器都存储数据,能存储的数据比较多

kafka性能非常好:使用了直接内存,零拷贝。

rocketmq借鉴了kafka、rabbitmq。

rabbitmq适合企业内部使用。一般企业使用rabbitmq就够了,一般企业5个节点就已经很多了。

上面说的RabbitMq吞吐量比较低、消息积累会影响性能这些,其实在RabbitMq设计了Stream队列之后,这些都已经解决了。所以说RabbitMq的缺点只是对其过去版本的总结,现有的版本已经提升了很多了。因为加入了Stream队列这些,所以Stream队列借助了Kafka、RocketMq的思想。

另外上面说的RabbitMq吞吐量比较低、消息积累会影响性能这些是因为Classic和Quorum队列在收到消息时,如果是持久化消息,则会将消息储存在内存中,同时也会写入磁盘,所以消息量大的时候会内存会不断变小,吞吐量和性能就会变差。而RocketMQ或者kafka是会存在本地的,需要的时候再读取到内存,没那么耗性能。

1.kafka、rocketmq吞吐量高,是因为会将数据分散存储。那他会把收到的消息数据存到内存?

不会,他们是记录到日志中的。

但是在操作系统层面,当应用程序写入一个文件时,文件内容并不会直接写入到硬件当中,而是会先写入到操作系统中的一个缓存PageCache中。但可以认为会把收到的消息数据存到内存。

2.Stream队列不会将消息存在内存?不会。

3.Stream类似Kafka、RocketMq,以append-only log方式将消息记录到日志文件,然后消费的时候也是通过offset方式进行消费。

4.Quorum、Classic队列没有offset,不能快速找到数据,才需要将全部数据存放到内存。

这3个主要中间件一直在更新,一直在发展,越来越像。

activemq现在很少使用了,主要是用在很老的项目。

rocketmq适合业务比较多(topic比较多)的场景。

rabbitmq里面的routingkey和rocketmq里面的tag的区别?

routingkey在用在从交换机转换到哪个队列的,用在服务端。而tag用于消息过滤、消息路由、业务分类。

mq一般使用推模式。

rocketmq和kafka在互联网公司用的比较多,rabbitmq在中小公司用的比较多。

kafka一般用在大数据场景。

Rabbitmq

Rabbitmq单机搭建

略。

Rabbitmq集群(多节点的集群有两种方式模式)

a.默认的普通集群模式

不支持高可用:master宕机需要手动重启。

消息也不可靠:消息只存在一个节点中。

集群的各个节点之间只会有相同的元数据(比如队列、交换机。)。另外消息不会进行冗余,只存在一个节点中。消费时,如果消费的不是存有数据的节点, RabbitMQ会临时在节点之间进行数据传输,将消息从存有数据的节点传输到消费的节点。这种集群模式的消息可靠性不是很高。并且,这种集群模式也不支持高可用,即当某一个节点服务挂了后,需要手动重启服务,才能保证这一部分消息能正常消费。

所以这种集群模式只适合一些对消息安全性不是很高的场景。而在使用这种模式时,消费者应该尽量的连接上每一个节点,减少消息在集群中的传输。

b.镜像模式(会主动同步数据,而不是被动)

高可用:会自动选举新master节点。

消息可靠:消息会存在各节点。

但吞吐量不高:消息存在各节点,这样会消耗大量带宽。

这种模式是在普通集群模式基础上的一种增强方案,这也就是RabbitMQ的官方HA高可用方案。需要在搭建了普通集群之后再补充搭建。其本质区别在于,这种模式会在镜像节点中间主动进行消息同步,而不是在客户端拉取消息时临时同步。

并且在集群内部有一个算法会选举产生master和slave,当一个master挂了后,也会自动选出一个来。从而给整个集群提供高可用能力。

这种模式的消息可靠性更高,因为每个节点上都存着全量的消息。而他的弊端也是明显的,集群内部的网络带宽会被这种同步通讯大量的消耗(每个节点都存在全量数据,需要进行大量数据同步,不像kafka将数据分散存储),进而降低整个集群的性能。这种模式下,队列数量最好不要过多。

Exchanges:消息发送到RabbitMQ中后,会首先进入一个交换机,然后由交换机负责将数据转发到不同的队列中。即交换机的作用是可以对发送到RabbitMq Server的消息进行转发,转发到不同的队列中。

RabbitMQ中有多种不同类型的交换机来支持不同的路由策略。从Web管理界面就能看到,在每个虚拟主机中,RabbitMQ都会默认创建几个不同类型的交换机来。

交换机多用来与生产者打交道。生产者发送的消息通过Exchange交换机分配到各个不同的Queue队列上,而对于消息消费者来说,通常只需要关注自己感兴趣的队列就可以了。

队列消息删除机制:包括手动确认、自动确认、定时删除(TTL)等方式。

可以通过给队列设置TTL属性,来定时删除消息。

// 声明一个队列

String queueName = "testQueue";

Map<String, Object> arguments = new HashMap<>();

// 设置消息的TTL为30000毫秒(30秒)

arguments.put("x-message-ttl", 30000);

channel.queueDeclare(queueName, true, false, false, arguments);

stream队列中的消息被消费了,是不会被删除的,而是通过类似kafka offset去定位从哪里去消费(只有Stream队列类型才会有offset概念,Classic经典队列没有),因为还需要发到其它消费组去推送消息。当过了一段时间后会统一删除队列的消息。

rabbitmq和kafka一样,同一个消费组的只有一个消费者才会消费消费消息。不同消费者组会重复消费一个消息。

rabbitmq没有类似kafka topic的概念,只有queue、exchanges的概念,只是在exchanges有topic的概念。而kafka、rocketmq有topic、queue的概念。

exchanges topic的概念?(Topic Exchange 类型支持通配符匹配路由键,然后将消息发送到符合特定规则的Queue 中)

RabbitMQ 和 Kafka 在消息队列架构上有一些不同之处。在 RabbitMQ 中,消息传递主要围绕着 Exchange 和 Queue 来进行,而 Exchange 中的类型包括 Direct、Fanout、Topic 和 Headers。

a.Exchange:Exchange 接收来自生产者的消息,并根据特定的规则将消息路由到一个或多个与之绑定的 Queue 中。Exchange 的类型决定了消息的路由方式。

b.Queue:Queue 是消息的容器,消费者从中接收消息。消息在进入 Queue 之前通过 Exchange 进行路由。

在 RabbitMQ 中,并没有像 Kafka 中的 Topic 概念那样直接映射到 Exchange。相对应的是,在 RabbitMQ 中的 Exchange 类型中,Topic Exchange 类型支持通配符匹配路由键,类似于 Kafka 中的 Topic,但并不是一一对应的概念。

因此,在 RabbitMQ 中,你可以使用 Topic Exchange 来实现类似于 Kafka Topic 的功能,通过设置特定的 Routing Key 和 Binding Key,将消息发送到符合特定规则的 Queue 中,实现灵活的消息路由和订阅功能。

Direct Exchange:将消息路由到绑定键(Binding Key)与消息的路由键(Routing Key)完全匹配的队列中。

Fanout Exchange:将消息广播到所有绑定的队列中。

Topic Exchange:根据消息的路由键和绑定键的模式进行匹配,将消息路由到一个或多个符合匹配规则的队列中。

Headers Exchange:使用消息的头部信息(headers)而不是路由键来决定消息的路由方式。

rabbitmq Exchanges四种类型:Direct、Fanout、Topic 和 Headers的java代码使用案例:

1.Direct Exchange

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

public class DirectExchangeExample {
    private static final String EXCHANGE_NAME = "direct_exchange";
    private static final String QUEUE_NAME = "direct_queue";
    private static final String ROUTING_KEY = "direct_routing_key";

    public static void main(String[] args) throws Exception {
        // 创建连接和通道
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {

            // 声明一个 Direct Exchange
            channel.exchangeDeclare(EXCHANGE_NAME, "direct");

            // 声明一个队列
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);

            // 将队列绑定到 Exchange,并指定 Routing Key
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);

            // 发送消息到 Exchange,并指定 Routing Key
            String message = "Hello, Direct Exchange!";
            channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, message.getBytes("UTF-8"));
            System.out.println("Sent message: '" + message + "'");
        }
    }
}

解释:

在这个例子中,我们创建了一个 Direct Exchange 名称为 direct_exchange。

创建了一个队列 direct_queue,并将它绑定到 direct_exchange,绑定时使用了路由键 direct_routing_key。

发送了一条消息到 Exchange,并指定了与队列绑定时相同的路由键 direct_routing_key。

2 Fanout Exchange

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

public class FanoutExchangeExample {
    private static final String EXCHANGE_NAME = "fanout_exchange";
    private static final String QUEUE_NAME1 = "fanout_queue1";
    private static final String QUEUE_NAME2 = "fanout_queue2";

    public static void main(String[] args) throws Exception {
        // 创建连接和通道
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {

            // 声明一个 Fanout Exchange
            channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

            // 声明两个队列
            channel.queueDeclare(QUEUE_NAME1, false, false, false, null);
            channel.queueDeclare(QUEUE_NAME2, false, false, false, null);

            // 将队列1绑定到 Exchange
            channel.queueBind(QUEUE_NAME1, EXCHANGE_NAME, "");

            // 将队列2绑定到 Exchange
            channel.queueBind(QUEUE_NAME2, EXCHANGE_NAME, "");

            // 发送消息到 Exchange
            String message = "Hello, Fanout Exchange!";
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
            System.out.println("Sent message: '" + message + "'");
        }
    }
}

解释:

在这个例子中,我们创建了一个 Fanout Exchange 名称为 fanout_exchange。

创建了两个队列 fanout_queue1 和 fanout_queue2,并将它们都绑定到 fanout_exchange。

发送了一条消息到 Exchange,消息会被广播到所有与 Exchange 绑定的队列上。

3.Topic Exchange

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

public class TopicExchangeExample {
    private static final String EXCHANGE_NAME = "topic_exchange";
    private static final String QUEUE_NAME1 = "topic_queue1";
    private static final String QUEUE_NAME2 = "topic_queue2";
    private static final String ROUTING_KEY1 = "topic.key1";
    private static final String ROUTING_KEY2 = "topic.key2";

    public static void main(String[] args) throws Exception {
        // 创建连接和通道
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {

            // 声明一个 Topic Exchange
            channel.exchangeDeclare(EXCHANGE_NAME, "topic");

            // 声明两个队列
            channel.queueDeclare(QUEUE_NAME1, false, false, false, null);
            channel.queueDeclare(QUEUE_NAME2, false, false, false, null);

            // 将队列1绑定到 Exchange,使用通配符路由键 "*.key1"
            channel.queueBind(QUEUE_NAME1, EXCHANGE_NAME, "*.key1");

            // 将队列2绑定到 Exchange,使用通配符路由键 "topic.*"
            channel.queueBind(QUEUE_NAME2, EXCHANGE_NAME, "topic.*");

            // 发送消息到 Exchange,使用路由键 "topic.key1"
            String message1 = "Hello, Topic Exchange - Key1!";
            channel.basicPublish(EXCHANGE_NAME, "topic.key1", null, message1.getBytes("UTF-8"));
            System.out.println("Sent message 1: '" + message1 + "'");

            // 发送消息到 Exchange,使用路由键 "topic.key2"
            String message2 = "Hello, Topic Exchange - Key2!";
            channel.basicPublish(EXCHANGE_NAME, "topic.key2", null, message2.getBytes("UTF-8"));
            System.out.println("Sent message 2: '" + message2 + "'");
        }
    }
}

解释:

在这个例子中,我们创建了一个 Topic Exchange 名称为 topic_exchange。

创建了两个队列 topic_queue1 和 topic_queue2,并分别使用不同的通配符路由键将它们绑定到 topic_exchange。

发送了两条消息到 Exchange,分别使用了符合队列绑定模式的路由键。

4.Headers Exchange

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

public class HeadersExchangeExample {
    private static final String EXCHANGE_NAME = "headers_exchange";
    private static final String QUEUE_NAME = "headers_queue";
    private static final Map<String, Object> HEADERS = new HashMap<>();

    public static void main(String[] args) throws Exception {
        // 创建连接和通道
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {

            // 声明一个 Headers Exchange
            channel.exchangeDeclare(EXCHANGE_NAME, "headers");

            // 声明一个队列
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);

            // 定义 headers 属性匹配规则
            HEADERS.put("header1", "value1");

            // 将队列绑定到 Exchange,指定 headers 匹配规则
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "", HEADERS);

            // 发送消息到 Exchange,并设置匹配的 headers 属性
            Map<String, Object> messageHeaders = new HashMap<>();
            messageHeaders.put("header1", "value1");
            String message = "Hello, Headers Exchange!";
            channel.basicPublish(EXCHANGE_NAME, "", new BasicProperties.Builder().headers(messageHeaders).build(), message.getBytes("UTF-8"));
            System.out.println("Sent message: '" + message + "'");
        }
    }
}

解释:

在这个例子中,我们创建了一个 Headers Exchange 名称为 headers_exchange。

创建了一个队列 headers_queue,并使用 headers 属性 header1=value1 将它绑定到 headers_exchange。

发送了一条消息到 Exchange,并设置了符合队列绑定 headers 属性的消息头。

rabbitmq实现顺序消费需要在单队列才能实现。

在一个worker(节点)中创建了VirtualHost、Queues后,集群中的其它节点也会有这个VirtualHosts、Queues。即集群中所有的节

点数据都是一样的,是会进行同步的。

消费者消费消息有推模式和拉模式

队列

Classic 经典队列(最为常用)

单机环境中(不是集群),拥有比较高的消息可靠性。因为可以持久化数据。

多节点下采用主从复制,可能会丢失数据。

Quorum 仲裁队列(针对Classic优化方式一)

(单机下通过持久化保证消息可靠性。集群下通过Raft协议保证分布式消息可靠性)

a.仲裁队列相比Classic经典队列,在分布式环境下对消息的可靠性保障更高。官方文档中表示,未来会使用Quorum仲裁队列代替传统Classic队列。

b.基于Raft一致性协议实现的一种新型的分布式消息队列,他实现了持久化,多备份的FIFO队列,主要就是针对RabbitMQ的镜像模式设计的。

c.仲裁队列的消息都是持久化的,同时队列的所有消息一直会保存在内存中。经典队列也一样,都会保存在内存中。

d.适合场景

Quorum队列更适合:需要队列长期存在,并且对容错、数据安全方面的要求比较高。对低延迟要求不高,且不需要临时消息的场景。例如 电商系统的订单,引入MQ后,处理速度可以慢一点,但是订单不能丢失。

e.不适合场景

响应的Quorum队列不适合使用的场景:

1、一些临时使用的队列:比如transient临时队列,exclusive独占队列,或者经常会修改和删除的队列。(即Quorum不支持临时队列和独占队列)

2、对消息低延迟要求高: 一致性算法会影响消息的延迟。

3、对数据安全性要求不高:Quorum队列需要消费者手动通知或者生产者手动确认。

4、队列消息积压严重 : 如果队列中的消息很大,或者积压的消息很多,就不要使用Quorum队列(因为Classic和Quorum队列在收到消息时,如果是持久化消息,则会将消息储存在内存中,同时也会写入磁盘,所以消息量大的时候会内存会不断变小,吞吐量和性能就会变差)。Quorum队列当前会将所有消息始终保存在内存中,直到达到内存使用极限。

所以,对于消息特别多且消息积压严重,我们一般不使用activemq/rabbitmq的经典队列和仲裁队列。

rabbimq中Quorum队列和Classic经典队列的区别总结:

1.可靠性方面:单节点时,经典队列和仲裁队列可靠性都很高。多节点集群下,经典队列仅采用主从复制,即一个主节点和多个从节点,主节点负责处理所有的读写操作,从节点则复制主节点的数据,数据可能会不一致。而仲裁队列采用了基于Raft一致性协议实现数据一致性,需要又集群中多半节点同意确认后,才会写入到队列中,可靠性更高。

单机下通过持久化保证消息可靠性。集群下通过Raft协议保证分布式消息可靠性

2.持久化:经典队列可以设置是否持久化,而仲裁队列都是持久化。同时队列的所有消息一直会保存在内存中。经典队列也一样,都会保存在内存中。所以对于消息特别多且消息积压严重,我们一般不使用activemq/rabbitmq的经典队列和仲裁队列。

3.仲裁队列大部分功能都是在经典队列基础上做减法,比如仲裁队列相比经典队列少了是否持久化、是否独占这些属性配置。

4.仲裁队列有毒消息机制,而经典队列没有。

Stream队列(针对Classic优化方式二,推荐)

Stream队列不会把所有的消息都放在内存中,需要的时候才从磁盘上去取(通过offset快速找到,而不像Quorum、Classic队列没有offset,不能快速找到数据,才需要将全部数据存放到内存)

--

RabbitMQ编程模型

(可以使用下面三种方式来使用RabbitMq)

从原生API、SpringBoot集成(使用最多)、SpringCloudStream集成。

RabbitMQ高级使用场景

1.Header路由

2.分组消费模式

3.死信队列

4.消费优先级与流量控制

5.远程数据分发插件

6.懒队列(懒加载) Lazy Queue

RabbitMQ从3.6.0版本开始,就引入了懒队列的概念。懒队列会尽可能早的将消息内容保存到硬盘当中,并且只有在用户请求到时,才临时从硬盘加载到RAM内存当中。

懒队列的设计目标是为了支持非常长的队列(数百万级别)。队列可能会因为一些原因变得非常长-也就是数据堆积。

懒队列适合消息量大且长期有堆积的队列,可以减少内存使用,加快消费速度。但是这是以大量消耗集群的网络及磁盘IO为代价的(即尽早将消息持久化,尽早多节点同步)。

7.消息分片存储插件

如何提高吞吐量?

上面的懒队列其实就是针对这个问题的一种解决方案。但是很显然,懒队列的方式属于治标不治本。真正要提升RabbitMQ单队列的吞吐量,还是要从数据也就是消息入手,只有将数据真正的分开存储才行。RabbitMQ提供的Sharding插件,就是一个可选的方案。他会真正将一个队列中的消息分散存储到不同的节点上,并提供多个节点的负载均衡策略实现对等的读与写功能。

RabbitMQ使用中的常见问题

一、如何使用RabbitMQ保证消息不丢失?

二、如何保证消息幂等

三、如何保证消息的顺序?

四、关于RabbitMQ的数据堆积问题

五、RabbitMQ的备份与恢复

六、RabbitMQ的性能监控

七、搭建HAProxy,实现高可用集群(在集群基础上增加负载均衡的能力,将客户端的请求能够尽量均匀的分配到集群中各个节点上)

总结:

基于MQ的事件驱动机制,给庞大的互联网应用带来了不一样的方向。MQ的异步、解耦、削峰三大功能特点在很多业务场景下都能带来极大的性能提升,在日常工作过程中,应该尝试总结这些设计的思想。虽然MQ的功能,说起来比较简单,但是随着MQ的应用逐渐深化,所需要解决的问题也更深入。对各种细化问题的挖掘程度,很大程度上决定了开发团队能不能真正Hold得住MQ产品。通常面向互联网的应用场景,更加注重MQ的吞吐量,需要将消息尽快的保存下来,再供后端慢慢消费。而针对企业内部的应用场景,更加注重MQ的数据安全性,在复杂多变的业务场景下,每一个消息都需要有更加严格的安全保障。而在当今互联网,Kafka(吞吐量大)是第一个场景(即面向互联网的应用场景)的不二代表,但是他会丢失消息的特性,让kafka的使用场景比较局限。RabbitMQ作为一个老牌产品,是第二个场景(即对企业内部的应用场景)最有力的代表。当然,随着互联网应用不段成熟,也不断有其他更全能的产品冒出来,比如阿里的RocketMQ(所以rocketmq是rabbitmq和kafka之后推出的)以及雅虎的Pulsar。但是不管未来MQ领域会是什么样子,RabbitMQ依然是目前企业级最为经典也最为重要的一个产品。他的功能最为全面,周边生态也非常成熟,并且RabbitMQ有庞大的Spring社区支持,本身也在吸收其他产品的各种优点,持续进化,所以未来RabbitMQ的重要性也会更加凸显。

相关推荐
雪碧聊技术6 分钟前
RabbitMQ7:消息转换器
rabbitmq·消息转换器·messageconvert
Mr.Demo.13 分钟前
[RabbitMQ] 保证消息可靠性的三大机制------消息确认,持久化,发送方确认
分布式·rabbitmq
小扳25 分钟前
微服务篇-深入了解使用 RestTemplate 远程调用、Nacos 注册中心基本原理与使用、OpenFeign 的基本使用
java·运维·分布式·后端·spring·微服务·架构
LightOfNight1 小时前
Redis设计与实现第14章 -- 服务器 总结(命令执行器 serverCron函数 初始化)
服务器·数据库·redis·分布式·后端·缓存·中间件
cnsxjean10 小时前
SpringBoot集成Minio实现上传凭证、分片上传、秒传和断点续传
java·前端·spring boot·分布式·后端·中间件·架构
果冻~11 小时前
【nodejs表单数据的中间件&错误级别的中间件】
中间件
skaiuijing16 小时前
Sparrow系列拓展篇:对信号量应用问题的深入讨论
c语言·开发语言·算法·中间件·操作系统
桃园码工16 小时前
3-测试go-redis+redsync实现分布式锁 --开源项目obtain_data测试
redis·分布式·golang
sx_170616 小时前
Spark面试题
大数据·分布式·spark
wclass-zhengge18 小时前
02微服务系统与设计(D1_走出微服务误区:避免从单体到分布式单体)
分布式·微服务·架构