五千字总结RabbitMQ核心知识及场景运用

195 阅读16分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

RabbitMQ

MQ

什么是MQ

MQ(message queue),本质上是个队列,FIFO先进先出,队列中的内容是消息,是一种通信机制。

为什么用MQ

特点:削峰、解耦、异步

削峰

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

image.png

解耦

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

异步

有些服务间调用是异步的,例如 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 服务还能及时的得到异步处理成功的消息

image.png

RabbitMQ

RabbitMQ是一个消息中间件,负责接收,存储并转发消息数据

四大核心

生产者

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

交换机

是RabbitMQ重要的部件,负责接收生产者发送的消息,并且将消息推送到队列中;交换机可以将消息推送到特定的队列或者多个队列,或者把消息丢失,这根据交换机类型决定

队列

是RabbitMQ内部使用的一种数据结构,是消息存储的地方,仅受主机的内存和磁盘限制的约束;生产者可以直接发消息到队列,消费者从队列中接收消息

消费者

消费者是一个等待接收消息的程序,生产者和消费者可以是同一个程序,也可以是不同程序

名词介绍

image.png

Broker

接收和分发消息的应用,RabbitMQ Server 就是Message Broker

Virtual host

出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等

Connection

publisher/consumer 和 broker 之间的 TCP 连接

Channel

如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCPConnection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。 l Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销

Exchange

message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。

常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout(multicast)

Queue

消息最终被送到这里等待 consumer 取走

Binding

exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

核心部分

简单模式(Hello World)

“ P”是生产者,“ C”是消费者。中间的框是一个队列-RabbitMQ 代表使用者保留的消息缓冲区(队列)

image.png

工作队列(World queues)

又称任务队列,主要思想是一个队列对应多个消费者,避免大量数据同时只能被一个消费者消费,所有消息只能按一个接一个消费;

分发机制

1、轮询

默认情况下,rabbitmq将会按顺序派发每个任务给下一个消费者,平均而言,每个消费者将获得相同数量的消息,这种分发消息的方式称为轮询

2、不公平分发

通过设置参数 channel.basicQos(1);实现不公平分发策略使得能者多劳;

3、预取值

避免channel上的未确认消息缓冲区大小无限制增大,通过使用basic.qos 方法设置“预取计数”值来完成的,该值定义了通道上未确认消息的最大数量。

应答机制

消费者在接收到消息并且处理该消息之后,告诉rabbitmq它已经处理了,rabbitmq可以把该消息删除了

主要防止消息在发送过程中丢失的问题。

1、自动应答

消息发送后立即被认为已经传送成功, 没有对传递的消息数量进行限制,如果消费不及时,会导致消息积压,有可能会把内存耗尽,线程被系统杀死导致消息丢失。

仅适用于消费者能一直高速处理消息的情况下适用。

2、手动应答

因消费者程序导致失去连接(通道关闭,连接已关闭或TCP连接丢失),rabbitMQ会默认消息未成功消费,会进行重新投递消息,可以批量应答。

目前没有超时概念,消费者不确认消费,消息状态一直处于unacked状态

发布订阅模式(Publish/Subscribe)

image.png

生产者不直接操作队列,而是先发送消息到交换机,再由交换机将消息发送到绑定该交换机的队列

交换机Exchanges

RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列,只能将消息发送到交换机

交换机工作的内容:一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们

Exchange( ( 交换机 ) 只负责转发消息 ,不具备存储消息的能力 不具备存储消息的能力

Exchanges的类型

扇出(fanout) ,直连(direct),主题(topic),标题(headers)

默认交换机

实上是一个由 RabbitMQ 预先声明好的名字为空字符串的直连交换机( direct exchange )· 它有一个特殊的属性使得它对于简单应用特别有用处:那就是每个新建队列( queue )都会自动綁定到认交换机上,綁定的路由键( routing key )名称与队列名称相同。

广播模式(Fanout)

把交换机里的消息发送给所有绑定该交换机队列,不需要routingKey,转发消息到队列速度是最快的

image.png

路由模式(Routing)

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由 key)
  • 消息的发送方在 向 Exchange 发送消息时,也必须指定消息的 RoutingKey。
  • Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key 完全一致,才会接收到消息

image.png

但是它绑定的多个队列的 key 如果都相同,在这种情况下虽然绑定类型是 direct 但是它表现的就和 fanout 有点类似了

image.png

主题模式(Topics)

Topic模式是direct模式上的一种叠加,增加了模糊路由RoutingKey的模式,简单的可以理解为就是模糊的路由key匹配模式

routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开

*(星号)可以代替一个单词

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

image.png

当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了 如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct 了

发布确认模式(Publisher Confirms)

发布确认是解决消息不丢失的重要环节

1、生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式, 所有在该信道上面发布的消息都将会被指派一个唯一的ID

2、如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出

3、confirm 模式最大的好处在于他是异步的;程序可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,程序同样可以在回调方法中处理该 nack 消息。

开启发布确认的方法:调用方法 confirmSelect

单个确认发布

同步确认发布的方式,发布一个消息之后只有它被确认发布,后续的消息才能继续发布;

waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常

缺点:发布速度特别的慢,如果没有确认发布的消息就会阻塞所有后续消息的发布

批量确认发布

先发布一批消息然后一起确认可以极大地提高吞吐量;这种方案仍然是同步的,也一样阻塞消息的发布

缺点::当发生故障导致发布出现问题时,无法定位哪个消息出现问题,必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息

异步确认发布

最佳性能和资源使用,在出现错误的情况下可以很好地控制,利用回调函数来达到消息可靠性传递,但是实现起来稍微难些

持久化机制

持久化是为提高rabbitmq消息的可靠性,防止在异常情况(重启,关闭,宕机)下数据的丢失

持久化分为三个部分: 交换器的持久化队列的持久化消息的持久化

交换机持久化

交换器的持久化是通过声明交换机时,将durable参数设置为true实现的。如果交换器不设置持久化,那么rabbitmq服务重启之后,相关的交换器数据将会丢失

队列持久化

队列的持久化是通过声明队列时,将durable参数设置为true实现的。如果队列不设置持久化,那么rabbitmq服务重启之后,相关的队列元数据将会丢失,而消息是存储在队列中的,所以队列中的消息也会被丢失

消息持久化

队列的持久化只能保证其队列本身的元数据不会被丢失,但是不能保证消息不会被丢失。所以消息本身也需要被持久化,可以在投递消息前设置属性deliveryMode为2即可

高级部分

死信队列

死信:消费者在从队列中获取消息进行消费时,因为某些原因导致有些消息无法被消费,如果这样的消息没有后续的处理就变成了死信;有死信消息自然就有了死信队列。

死信队列也是一个普通队列,当被别的队列声明成死信队列时,只是这两个队列直接的关系有了关联,除此之外没任何区别

来源:

  • 消息TTL过期
  • 队列达到最大值
  • 消息被拒绝

场景

1.png

延迟队列

延迟队列内部是有序的,最重要的特性就体现在它的延时属性上,简单来说就是用来存放需要在指定时间后被处理的元素的队列

场景

  • 订单在三十分钟之内未支付则自动取消
  • 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

TTL

是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间

消息设置TTL:作用于当前消息

队列设置TTL:作用于整个队列的消息

注意:

RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列或者丢弃,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

发布确认高级

问题

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。

解决

通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。

Mandatory :参数接收一个boolean值

true-交换机无法将消息进行路由时,会将该消息返回给生产者

false-交换机无法将消息进行路由时,直接丢弃

备份交换机

有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。

但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。

而且设置 mandatory 参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?

在 RabbitMQ 中,有一种备份交换机的机制存在,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。

幂等性

用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用

解决思路

MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。

业界主流的幂等性有两种操作:a.唯一 ID+指纹码机制,利用数据库主键去重, b.利用 redis 的原子性去实现

唯一ID+ 指纹码机制

指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中,

优势:实现简单就一个拼接,然后查询判断是否重复;

劣势:在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式

Redis原子性

利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费

优先队列

场景

客户分等级,大客户优先级高,优先处理,小客户优先级低,可以晚点处理。

设置

x-max-priority:1到255之间的正整数,推荐设置1到10之间的数值(如果设置太高比较吃内存和 CPU),表示队列应该支持的最大优先级;数字越大代表优先级越高

惰性队列

RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念;它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储

  1. 接收到消息后直接存入磁盘而非内存
  2. 消费者要消费消息时才会从磁盘中读取并加载到内存

虽然惰性队列性能稳定,但是增加了一些磁盘的IO,从消息的发送要接收会有一定的延迟,但也在可以接受的范围内

注意

RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候

设置

要设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可。

可以通过命令行将一个运行中的队列修改为惰性队列:^lazy-queue$ 用来匹配队列的名称,匹配到的就设置为惰性队列。

场景

消息堆积时,设置成惰性队列,以便存储更多消息