文章目录
-
- 一、RabbitMQ简介(可保证消息不丢失/路由方式灵活/高可用/支持多种语言)
- 二、消息的发送与消费
- 三、RabbitMQ进阶
- 四、相关问题
-
- 4.1 RabbitMQ上的一个queue中存放的message是否有数量限制(无)
- 4.2 如何确保消息正确地发送至RabbitMQ(发送方确认/异步,即等待确认时可以继续发送消息)
- 4.3 如何确保消息接收方消费了消息(消费方消息确认)
- 4.4 如何避免消息重复投递或重复消费(利用业务上的唯一性ID)
- 4.5 消息基于什么传输(TCP)
- 4.6 消息怎么路由(fanout/direct/topic/headers)
- 4.7 如何确保消息不丢失(生产者/Rabbit/消费者)
- 4.8 如何保证消息的顺序性(在消息体内存可以排序的唯一性id)
- 4.9 RabbitMQ交换器有哪些类型(fanout/direct/topic/headers)
- 4.10 RabbitMQ队列与消费者的关系
- 4.11 如何解决消息队列的延时以及过期失效问题
- 4.12 怎么保证消息队列的高可用(镜像集群)
- 4.13 Kafka、ActiveMQ、RabbitMQ、RocketMQ对比
一、RabbitMQ简介(可保证消息不丢失/路由方式灵活/高可用/支持多种语言)
RabbitMQ是采用Erlang语言实现AMQP(高级消息队列协议)的消息中间件,它最初起源于金融系统,用于在分布式系统中存储转发消息。
RabbitMO的具体特点:
1、可靠性:RabbitMO使用一些机制来保证可靠性,如持久化、传输确认及发布确认等。
2、灵活的路由:在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMO已经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。
3、扩展性:多个RabbitMO节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。
4、高可用性:队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。
5、支持多种协议:RabbitMQ除了原生支持AMQP协议,还支持STOMP、MQTT等多种消息中间件协议。
6、多语言客户端:RabbitMQ几乎支持所有常用语言,比如Java、Python、Ruby、PHP、C#、JavaScript等。
7、管理界面:RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。
8、插件机制:RabbitMO提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。
1.1 RabbitMQ概念
1.1.1 生产者和消费者(消息包括:消息体和标签)
RabbitMQ整体上是一个生产者与消费者模型,RabbitMQ的整体模型架构:
- Producer
生产者,就是投递消息的一方。消息一般可以包含2个部分:消息体(payload)和标签(Label)
。在实际应用中,消息体一般是一个带有业务逻辑结构的数据,比如一个json字符串。当然可以进一步对这个消息体进行序列化操作。消息的标签用来表述这条消息,比如一个交换器的名称和一个路由键。生产者把消息交由RabbitMO,RabbitMQ之后会根据标签把消息发送给感兴趣的消费者(Consumer)。 - Consumer
消费者,就是接收消息的一方。消费者连接到RabbitMQ服务器,并订阅到队列上。当消费者消费一条消息时,只是消费消息的消息体(payload)。在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只有消息体,消费者也只会消费到消息体,也就不知道消息的生产者是谁,当然消费者也不需要知道。 - Broker
消息中间件的服务节点。对于RabbitMQ来说,一个RabbitMQ Broker可以简单地看作一个RabbitMQ服务节点,或者RabbitMQ服务实例。大多数情况下也可以将一个RabbitMQ Broker看作一台RabbitMQ服务器。
生产者将消息存入RabbitMQ Broker,以及消费者从Broker中消费数据的整个流程:
生产者将业务方数据进行可能的包装,之后封装成消息,发送到Broker中。消费者订阅并接收消息,经过可能的解包处理得到原始的数据,之后再进行业务处理逻辑。
这个业务处理逻辑并不一定需要和接收消息的逻辑使用同一个线程。消费者进程可以使用一个线程去接收消息,存入到内存中,比如使用Java中的BlockingQueue。业务处理逻辑使用另一个线程从内存中读取数据,这样可以将应用进一步解耦,提高整个应用的处理效率。
1.1.2 队列
Queue:队列,是RabbitMQ的内部对象,用于存储消息。
多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理:
RabbitMQ不支持队列层面的广播消费
,如果需要广播消费,需要在其上进行二次开发。
1.1.3 交换器、路由键、绑定键
- Exchange交换器
生产者将消息发送到Exchange(交换器),由交换器将消息路由到一个或者多个队列中。如果路由不到,或许会返回给生产者,或许直接丢弃。
RabbitMQ中的交换器有四种类型,不同的类型有着不同的路由策略。 - RoutingKey路由键
生产者将消息发给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则,而这个Routing Key需要与交换器类型和绑定键(BindingKey)联
合使用才能最终生效。
在交换器类型和绑定键(BindingKey)固定的情况下,生产者可以在发送消息给交换器时,通过指定RoutingKey来决定消息流向哪里。 - Binding绑定
RabbitMQ中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键(BindingKey),这样RabbitMQ就知道如何正确地将消息路由到队列了:
所示。
生产者将消息发送给交换器时,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的BindingKey。
BindingKey并不是在所有的情况下都生效,它依赖于交换器类型,比如fanout类型的交换器就会无视BindingKey,而是将消息路由到所有绑定到该交换器的队列中。
交换器相当于投递包裹的邮箱,RoutingKey相当于填写在包裹上的地址,BindingKey相当于包裹的目的地,当填写在包裹上的地址和实际想要投递的地址相匹配时,那么这个包裹就会被正确投递到目的地,最后这个目的地的“主人”一一队列可以保留这个包裹。如果填写的地址出错,邮递员不能正确投递到目的地,包裹可能会回退给寄件人,也有可能被丢弃。
1.1.4 常用的交换器类型(fanout与交换器绑定的所有队列/direct路由键和绑定键完全匹配的队列/topic路由键和绑定键模式匹配的队列/heders属性匹配的队列)
RabbitMQ常用的交换器类型有fanout、direct、topic、headers这四种。
- fanout(与交换器绑定的所有队列)
它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。 - direct(路由键和绑定键完全匹配的队列)
它会把消息路由到那些BindingKey和RoutingKey完全匹配的队列中。 - topic(路由键和绑定键模式匹配的队列)
topic类型的交换器在匹配规则上进行了扩展,它与direct类型的交换器相似,也是将消息路由到BindingKey和RoutingKey相匹配的队列中,但这里的匹配规则有些不同,它约定:
RoutingKey为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如“com.rabbitmq.client”、“java.util.concurrent”、“com.hidden…client’”;
BindingKey和RoutingKey一样也是点号“.”分隔的字符串;
BindingKey中可以存在两种特殊字符串“*”和“#”,用于做模糊匹配,其中“#”用于匹配一个单词,“#”用于匹配多规格单词(可以是零个)。
示例:路由键为“com.rabbitmq.client’”的消息会同时路由到Queue1和Queue2;
路由键为“com.hidden.client’”的消息只会路由到Queue2中;
路由键为“com.hidden.demo”的消息只会路由到Queue2中;
路由键为“java.rabbitmq.demo”的消息只会路由到Queue1中;
路由键为“java.util.concurrent”的消息将会被丢弃或者返回给生产者(需要设置mandatory参数),因为它没有匹配任何路由键。
- heders(headers属性匹配)
headers类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。在绑定队列和交换器时制定一组键值对,当发送消息到交换器时,RabbitMQ会获取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
1.1.5 生产者和消费者工作的基本流程
- 生产者发送消息
1、生产者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)。
2、生产者声明一个交换器,并设置相关属性,比如交换机类型、是否持久化等。
3、生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等。
4、生产者通过路由键将交换器和队列绑定起来。
5、生产者发送消息至RabbitMO Broker,其中包含路由键、交换器等信息。
6、相应的交换器根据接收到的路由键查找相匹配的队列。
6.1 如果找到,则将从生产者发送过来的消息存入相应的队列中。
6.2 如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者。
7、关闭信道。
8、关闭连接。 - 消费者消费消息
1、消费者连接到RabbitMO Broker,建立一个连接(Connection),开启一个信道(Channel)。
2、消费者向RabbitMO Broker请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备工作。
3、等待RabbitMO Broker回应并投递相应队列中的消息,消费者接收消息。
4、消费者确认(ack)接收到的消息。
5、RabbitMO从队列中删除相应已经被确认的消息。
6、关闭信道。
7、关闭连接。
此处引入了两个新的概念:Connection和Channel。无论是生产者还是消费者,都需要和RabbitMQ Broker建立连接,这个连接就是一条TCP连接,也就是Connection。一旦TCP连接建立起来,客户端紧接着可以创建一个AMQP信道(Channel),每个信道都会被指派一个唯一的ID。信道是建立在Connection之上的虚拟连接,RabbitMQ处理的每条AMQP指令都是通过信道完成的。
RabbitMQ采用类似NIO的做法,选择TCP连接复用,不仅可以减少性能开销,同时也便于管理。
每个线程把持一个信道,所以信道复用了Connection的TCP连接。同时RabbitMQ可以确保每个线程的私密性,就像拥有独立的连接一样。当每个信道的流量不是很大时,复用单一的Connection可以在产生性能瓶颈的情况下有效地节省TcP连接资源。但是当信道本身的流量很大时,这时候多个信道复用一个Connection就会产生性能瓶颈,进而使整体的流量被限制了。此时就需要开辟多个Connection,将这些信道均摊到这些Connection中,至于这些相关的调优策略需要根据业务自身的实际情况进行调节。
1.2 AMQP协议
RabbitMQ就是AMQP协议的Erlang的实现(当然RabbitMQ还支持STOMP2、MQTT3等协议)。AMQP的模型架构和RabbitMQ的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定。当生产者发送消息时所携带的RoutingKey与绑定时的BindingKey相匹配时,消息即被存入相应的队列之中。消费者可以订阅相应的队列来获取消息。
AMQP协议本身包括三层。
1、Module Layer:位于协议最高层,主要定义了一些供客户端调用的命令,.客户端可以利用这些命令实现自己的业务逻辑。
2、Session Layer:位于中间层,主要负责将客户端的命令发送给服务器,再将服务端的应答返回给客户端,主要为客户端与服务器之间的通信提供可靠性同步机制和错误处理。
3、Transport Layer:位于最底层,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示等。
二、消息的发送与消费
2.1 连接RabbitMQ(创建Connection,再创建Channel)
连接RabbitMQ的2种方式:
//在给定的参数(P地址、端口号、用户名、密码等)下连接RabbitMQ
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(USERNAME);
factory.setPassword(PASSWORD);
factory.setvirtualHost(virtualHost);
factory.setHost(IP_ADDRESS);
factory.setport(PORT);
Connection conn = factory.newConnection();
//使用URI的方式连接RabbitMQ
ConnectionFactory factory = new ConnectionFactory();
factory.seturi("amqp://userName:password@ipAddress:portNumber/virtualHost");
Connection conn = factory.newConnection();
Connection接口被用来创建一个Channel:
Channel channel = conn.createChannel();
在创建之后,Channel可以用来发送或者接收消息了。
Connection可以用来创建多个Channel实例,但是Channel实例不能在线程间共享,应用程序应该为每一个线程开辟一个Channel
。多线程间共享Channel实例是非线程安全的。
通常情况下,在调用createxxx或者newXXX方法之后,我们可以简单地认为Connection或者Channel已经成功地处于开启状态。
2.2 使用交换器和队列(声明交换器和队列,再绑定)
在使用交换器和队列之前应该先声明,示例:
channel.exchangeDeclare(exchangeName,"direct",true);
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName,exchangeName,routingKey);
上面创建了一个持久化的、非自动删除的、绑定类型为direct的交换器,同时也创建了一个非持久化的、排他的、自动删除的队列(此队列的名称由RabbitMQ自动生成)。这里的交换器和队列也都没有设置特殊的参数。
上面的代码也展示了如何使用路由键将队列和交换器绑定起来。上面声明的队列具备如下特性:只对当前应用中同一个Connection层面可用,同一个Connection的不同Channel可共用,并且也会在应用连接断开时自动删除。
如果要在应用中共享一个队列,可以做如下声明:
channel.exchangeDeclare(exchangeName,"direct",true);
channel.queueDeclare(queueName,true,false,false,null);
channel.queueBind(queueName,exchangeName,routingKey);
这里的队列被声明为持久化的、非排他的、非自动删除的,而且也被分配另一个确定的已知的名称(由客户端分配而非RabbitMQ自动生成)。
生产者和消费者都可以声明一个交换器或者队列。如果尝试声明一个已经存在的交换器或者队列,只要声明的参数完全匹配现存的交换器或者队列,RabbitMQ就可以什么都不做,并成功返回。如果声明的参数不匹配则会抛出异常。
2.2.1 exchangeDeclare声明交换器/exchangeDelete删除交换器
Exchange.DeclareOk exchangeDeclare(String exchange,
String type,boolean durable,
boolean autoDelete,boolean internal,
Map<string,object>arguments)throws IOException;
这个方法的返回值是Exchange.Declare0K,用来标识成功声明了一个交换器。
exchange:交换器的名称。
type:交换器的类型。
durable:设置是否持久化。durable设置为true表示持久化,反之是非持久化。持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。
autoDelete:设置是否自动删除。autoDelete设置为true则表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。
internal:设置是否是内置的。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。
argument:其他一些结构化参数。
有声明创建交换器的方法,当然也有删除交换器的方法:
Exchange.Deleteok exchangeDelete(String exchange)throws IOException;
void exchangeDeleteNoWait(String exchange,boolean ifUnused)throws IOException;
Exchange.Deleteok exchangeDelete(String exchange,boolean ifUnused)throws IOException;
其中exchange表示交换器的名称,而ifUnused用来设置是否在交换器没有被使用的情况下删除。如果isUnused设置为true,则只有在此交换器没有被使用的情况下才会被删除:如果设置false,则无论如何这个交换器都要被删除。
2.2.2 queueDeclare声明队列/queueDelete删除队列/queuePurge清空队列中的内容
Queue.DeclareOk queueDeclare(String queue,
boolean durable,boolean exclusive,
boolean autoDelete,Map<String,Object>arguments)throws IOException;
queue:队列的名称。
durable:设置是否持久化。为true则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息。
exclusive:设置是否排他。为true则设置队列为排他的。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意三点:
1、排他队列是基于连接(Connection)可见的,同一个连接的不同信道(Channel)是可以同时访问同一连接创建的排他队列;
2、“首次”是指如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同;
3、即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景。
autoDelete:设置是否自动删除。为true则设置队列为自动删除。自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。不能把这个参数错误地理解为:“当连接到此队列的所有客户端断开时,这个队列自动删除”,因为生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列。
arguments:设置队列的其他一些参数。
生产者和消费者都能够使用queueDeclare来声明一个队列,但是如果消费者在同一个信道上订阅了另一个队列,就无法再声明队列了。必须先取消订阅,然后将信道置为“传输”模式,之后才能是声明队列。
与交换器对应,关于队列也有删除的相应方法:
Queue.Deleteok queueDelete(String queue)throws IOException;
Queue.Deleteok queueDelete(String queue,boolean ifUnused,boolean ifEmpty) throws IOException;
void queueDeleteNoWait(String queue,boolean ifUnused,boolean ifEmpty) throws IOException;
其中queue表示队列的名称,ifUnused可以参考上一小节的交换器。ifEmpty设置为true表示在队列为空(队列里面没有任何消息堆积)的情况下才能够删除。
与队列相关的还有一个有意思的方法queuePurge,区别于queueDelete,这个方法用来清空队列中的内容,而不删除队列本身:
Queue.Purgeok queuePurge(String queue)throws IOException;
2.2.3 queueBind绑定交换器和队列/queueUnbind将交换器和队列解绑/exchangeBind将交换器与交换器绑定
Queue.Bindok queueBind(String queue,
String exchange,String routingKey,
Map<String,Object>arguments)throws IOException;
queue:队列名称;
exchange:交换器的名称;
routingKey:用来绑定队列和交换器的路由键;
argument:定义绑定的一些参数。
不仅可以将队列和交换器绑定起来,也可以将已经被绑定的队列和交换器进行解绑。
Queue.Unbindok queueUnbind(String queue,
String