概念
RabbitMQ是一个消息中间件,而消息队列是分布式系统的重要组件。
说到消息队列首先就要说AMQP(高级消息队列协议)。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
AMQP模型(AMQP Model):一个由关键实体和语义表示的逻辑框架,遵从AMQP规范的服务器必须提供这些实体和语义。为了实现本规范中定义的语义,客户端可以发送命令来控制AMQP服务器。具体查阅百科
作用
栗子:登录流程
传统的应用执行流程为:
加入了消息队列之后:
- 应用解耦
传统应用中各个功能存在着依赖关系,当步骤1中的修改功能失败,从而阻塞或者服务降级处理,但这本不应妨碍我们步骤2的功能。
加入了消息队列之后,我们将修改上次登录时间这个消息放入至消息队列中,不管该消息究竟有没有被收到、被处理,我们直接可以返回相关页面数据。 - 异步处理
各应用之间,不再是通过远程调用执行功能,而是采用中间件的方式。
假如修改上次登录时间这个功能需要耗时100ms,那么在传统应用中,我们就要等待这100ms之后,才能进行接下来的步骤2。
加入了消息队列之后,我们只需消耗将消息放置在消息队列中的时间,就能立即进行接下来的操作。 - 削峰填谷
我们知道数据库系统的并发量是有极限的,因此在传统应用高并发的情况下,数据库是很容易崩的。
当我们引入了消息队列之后,我们可以设置消费者每次消费消息的个数,因此可以使数据库在不会崩且又保持较高并发的情况处理CRUD。
通过图表上看,我们可以知道: - 削峰就是在超高并发的时段,只让系统处理自己能够承受的数量。
- 填谷就是在并发量不高的时段,我们将之前积压在消息队列中的消息进行处理。
角色
宏观
- 生产者:进行生产消息的操作,将消息与路由键发送给指定的服务端的交换机中。
- 服务端:两个作用,1.该端的交换机根据路由键将消息转发至对应的队列。2.该端接收到消费者发来的确认信息,则删除对应的消息。
- 消费者:监听服务端的队列,一旦有消息,即可从队列中获取消息,消费完成之后,返送一个确认信息给服务端,表示已消费完该消息。
具体
因为rabbitmq是基于AMQP的,因此以下角色即遵从AMQP规范的服务器所提供的实体和语义。
- 连接:connection
一个TCP连接,用以客户端和服务端之间进行通信。 - 信道:channel
其实我们可以直接通过conection进行数据传输,但一个应用程序可能会要开启很多线程用以不断的生产消息和消费消息,因此会有许多connection即TCP连接的建立与销毁,但这些操作极其消耗资源。为此我们需采用多路复用连接技术,即建立一条独立的双向数据流通道。为会话提供物理传输介质。这就是信道的作用。 - 交换机:exchange
接收发布应用程序发送的消息,并根据一定的规则将这些消息路由到“消息队列”。 - 队列:queue
存储消息,直到这些消息被消费者安全处理完为止。 - 路由键:routingkey
- 绑定bind:exchange+queue+routingkey
定义了exchange和message queue之间的关联,提供路由规则。 - 等等
交换机类型及不同的作用
- Fanout:广播,将消息交给所有绑定到交换机的队列-不用指定路由键
- Direct:把消息交给符合指定routing key
的队列–(当生产者没有指定交换机,使用的默认交换机的交换机类型就是Direct,并且该路由键就是队列名) - Topic:可以使用通配符进行路由,把消息交给匹配routing pattern(路由模式)的队列,显得更为智能。在此需注意两个通配符,一个’
#
'表示匹配多个字段,一个 ’*
‘表示匹配一个字段。比如:以下两个使用通配符的路由键,com.i.love.you
与com.i
,使用’ # ',则两个都可以匹配到,而使用 ’ * ’ 则只能匹配到com.i。 - Headers:通过匹配AMQP消息的header而非路由键进行路由。但使用该类型使得效率不及其它类型交换机。
java实现rabbitmq大致流程
- 创建连接工厂
ConnectionFactory
,填充必要属性:host、port、username、password、virtual-host - 有连接工厂生产连接对象
Connection
-》工厂模式 - 通过第2步的连接对象创建信道
Channel
-》工厂模式 - 通过channel声明或创建队列
- 通过channel声明或创建交换机,要设置交换机类型
- 交换机绑定队列与路由键(若交换机类型为Fanout则不用指定路由键,否则需要指定)
- 生产者发送消息给指定交换机(通过basicPublish方法),消费者监听指定队列中(通过basicConsume方法)(需要
编写监听到消息之后的回调方法handleDelivery) - 生产者发送消息完毕后关闭信道channel.close();与连接connection.close();
高级特性
消息可靠传递
首先我们需先知道消息的传递流程,如下图。
comfirmCallback和returnCallback回调函数是我们实现可靠传递的保证。均在生产者方设置。
消息确认
我们如何实现消息的可靠传递,就是利用了消息确认ack这个机制。
- ack是我们需要记住的点,当我们设置ack为自动确认,即服务端会将发送给消费者的消息自动确认进行删除,而不管消费者是否真的成功消费了该消息。当我们设置ack为手动确认,即服务端必须要收到消费者发来的确认消息后才将队列上的消息进行删除操作。
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
手动签收,会返送rabbitmq一个确认信息,rabbitmq就会删除该消息了。
channel.basicNack(deliveryTag,true,true);
拒绝签收,重回队列。
channel.basicReject(deliveryTag,true);
拒绝签收。 - 设置每次从队列上拉取的消息的个数。
channel.basicQos(1);
->指定消费者从队列上拉取多少个消息,直到消费者发送确认信息,才拉取接下来的消息。–》削峰填谷:每次只拉取自己能够消化的消息量。
持久化
持久化,顾名思义就是当rabbitmq服务重启之后,交换机的数据、队列的数据、队列中的消息的数据依然存在。
将需要持久化的角色添加参数durable="true"
即可
- 当交换机没有持久化,rabbitmq重启之后,交换机中的配置比如绑定的队列和路由键就将丢失。至于消息是否丢失,我们需要知道的是,消息是存放在队列上的,交换机只是起到了路由的作用,它并不存储消息,所以无论交换机有没有持久化,都对消息本身没有影响。
- 当队列没有持久化,或者消息没有持久化,消息都将丢失。
过期时间ttl
对于某些消息或整个队列,我们设置其过期时间。
若我们没有设置过期时间,表示该消息永远不过期。但如果我们给消息和该消息所在的整个队列都设置了过期时间,那么该消息真正过期时间取自两者中的最小值。
- 给队列添加属性
x-message-ttl
<rabbit:queue auto-declare="true" id="test-queue-1" name="test-queue-1">
<rabbit:queue-arguments>
<!--给整个队列设置过期时间,value值为过期时间-->
<entry key="x-message-ttl" value="20000" value-ref="java.lang.Integer"/>
</rabbit:queue-arguments>
</rabbit:queue>
死信队列
- 当队列上的某个消息被拒签
- 队列达到最大长度
- 消息达到了过期时间ttl
只要有消息符合以上三个条件中的任何一个就成为了本来将被抛弃的消息,我们将该消息放入死信交换机,死信交换机根据路由键放入相应的死信队列中。
- 给队列添加属性
x-dead-letter-exchange
和x-dead-letter-routing-key
<rabbit:queue auto-declare="true" id="test-queue-2" name="test-queue-2">
<rabbit:queue-arguments>
<!--发送消息至该队列,value值为死信交换机名-->
<entry key="x-dead-letter-exchange" value="topicExchange" />
<!--发送给死信交换机的路由键-->
<entry key="x-dead-letter-routing-key" value="com.i.love.you" />
</rabbit:queue-arguments>
</rabbit:queue>
注意:判断消息是否为死信,需要消息处于队列第一个的位置,假若死信消息的前面还有消息没有被消费掉,则等待。
延迟队列
rabbitmq没有直接实现该特性,但我们可以使用死信队列+ttl轻松实现。
将消息放置在队列中,队列中的消息等待自己的过期时间到来,之后将消息发送给死信队列,由消费者消费。
<rabbit:queue auto-declare="true" id="test-queue-3" name="test-queue-3">
<rabbit:queue-arguments>
<!--value值为过期时间-->
<entry key="x-message-ttl" value="20000" value-ref="java.lang.Integer"/>
<!--发送消息至该队列,value值为死信交换机名-->
<entry key="x-dead-letter-exchange" value="topicExchange" />
<!--发送给死信交换机的路由键-->
<entry key="x-dead-letter-routing-key" value="com.i.*" />
</rabbit:queue-arguments>
</rabbit:queue>