文章目录
- [什么是 RabbitMQ?](#什么是 RabbitMQ?)
- [RabbitMQ 和 AMQP 是什么关系?](#RabbitMQ 和 AMQP 是什么关系?)
- [RabbitMQ 的核心组件有哪些?](#RabbitMQ 的核心组件有哪些?)
- [RabbitMQ 中有哪几种交换机类型?](#RabbitMQ 中有哪几种交换机类型?)
-
- [Direct Exchange(直连交换机)](#Direct Exchange(直连交换机))
- [Topic Exchange(主题交换机)](#Topic Exchange(主题交换机))
- [Headers Exchange(头部交换机)](#Headers Exchange(头部交换机))
- [Fanout Exchange(广播交换机)](#Fanout Exchange(广播交换机))
- [Default Exchange(默认交换机)](#Default Exchange(默认交换机))
- [RabbitMQ 如何保证消息不丢失/消息持久化?](#RabbitMQ 如何保证消息不丢失/消息持久化?)
- RabbitMQ中如何解决消息堆积问题?
- RabbitMQ中如何保证消息不被重复消费?
什么是 RabbitMQ?
RabbitMQ 是一个开源的消息中间件,使用 Erlang 语言开发。这种语言天生非常适合分布式场景,RabbitMQ 也就非常适用于在分布式应用程序之间传递消息。RabbitMQ 有非常多显著的特点:
消息传递模式:RabbitMQ 支持多种消息传递模式,包括发布/订阅、点对点和工作队列等,使其更灵活适用于各种消息通信场景。
消息路由和交换机:RabbitMQ 引入了交换机(Exchange)的概念,用于将消息路由到一个或多个队列。这允许根据消息的内容、标签或路由键进行灵活的消息路由,从而实现更复杂的消息传递逻辑。
消息确认机制:RabbitMQ 支持消息确认机制,消费者可以确认已成功处理消息。这确保了消息不会在传递后被重复消费,增加了消息的可靠性。
可扩展性:RabbitMQ 是高度可扩展的,可以通过添加更多的节点和集群来增加吞吐量和可用性。这使得 RabbitMQ 适用于大规模的分布式系统。
多种编程语言支持:RabbitMQ 提供了多种客户端库和插件,支持多种编程语言,包括 Java、Python、Ruby、Node.js 等,使其在不同技术栈中都能方便地集成和使用。
消息持久性:RabbitMQ 允许消息和队列的持久性设置,确保消息在 RabbitMQ 重新启动后不会丢失。这对于关键的业务消息非常重要。
灵活的插件系统:RabbitMQ 具有丰富的插件系统,使其可以扩展功能,包括管理插件、数据复制插件、分布式部署插件等。
管理界面:RabbitMQ 提供了一个易于使用的 Web 管理界面,用于监视和管理队列、交换机、连接和用户权限等。
总之,RabbitMQ 是一个功能丰富、高度可扩展且灵活的消息中间件,适用于各种分布式应用程序和消息通信需求。它的强大功能和广泛的社区支持使其成为一个流行的消息中间件解决方案。
RabbitMQ 和 AMQP 是什么关系?
RabbitMQ 和 AMQP 有着非常密切的关系,但是他们是属于完全不同的两个概念。
- AMQP : AMQP 不是一个具体的消息中间件产品,而是一个协议规范。他是一个开放的
消息产地协议
,是一种应用层的标准协议,为面向消息的中间件设计。AMQP 提供了一种统一的消息服务,使得不同程序之间可以通过消息队列进行通信。 SpringBoot 框架默认就提供了对 AMQP 协议的支持。 - RabbitMQ :RabbitMQ则是一个开源的消息中间件,是一个具体的软件产品。RabbitMQ 使用 AMQP 协议来实现
消息传递
的标准,但其实他也支持其他消息传递协议,如 STOMP 和 MQTT。RabbitMQ 基于 AMQP 协议定义的消息格式和交互流程,实现了消息在生产者、交换机、队列之间的传递和处理。
总之,AMQP 本质上是一个开放的标准,他不光可以被 RabbitMQ 实现,也可以被其他产品实现。通过这种标准的协议,实际上是可以在不同的消息中间件系统之间进行灵活的消息传递。只不过,目前具体实现这种标准的产品目前并不多,RabbitMQ 则是最有影响力的一个产品。因此,RabbitMQ 成了 AMQP 协议事实上的代表。SpringBoot 框架默认提供的 AMQP 协议支持底层也是基于 RabbitMQ 产品实现的。
RabbitMQ 的核心组件有哪些?
RabbitMQ的核心组件包括以下几部分,他们共同构成了 RabbitMQ 的基本架构:
- Broker:RabbitMQ服务器,负责接收和分发消息的应用。
- Virtual Host:虚拟主机,是RabbitMQ中的逻辑容器,用于隔离不同环境或不同应用程序的信息流。每个虚拟主机都有自己的队列、交换机等设置,可以理解为一个独立的RabbitMQ服务。
- Connection 连接:管理和维护与RabbitMQ服务器的TCP连接,生产者、消费者通过这个连接和 Broker 建立物理网络连接。
- Channel 通道:是在Connection 内创建的轻量级通信通道,用于进行消息的传输和交互。应用程序通过Channel进行消息的发送和接收。通常一个 Connection 可以建立多个 Channel。
- Exchange 交换机:交换机是消息的中转站,负责接收来自生产者的消息,并将其路由到一个或多个队列中。RabbitMQ 提供了多种不同类型的交换机,每种类型的交换机都有不同的消息路由规则。
- Queue 队列:队列是消息的存储位置。每个队列都有一个唯一的名称。消息从交换机路由到队列,然后等待消费者来获取和处理。
- Binding 绑定关系: Binding 是 Exchange 和 Queue 之间的关联规则,定义了消息如何从交换机路由到特定的队列。
此外,生产者和消费者也是RabbitMQ的核心组件,生产者负责发送消息到Exchange或者 Queue,消费者负责从Queue中订阅和处理消息。
这些核心组件共同构建了 RabbitMQ 的消息传递系统,他们协同工作才能实现消息的可靠传递、路由和业务处理等功能。
RabbitMQ 中有哪几种交换机类型?
RabbitMQ 支持多种交换机(Exchange)类型,每种类型都用于不同的消息路由和分发策略:
Direct Exchange(直连交换机)
这种交换机根据消息的路由键(Routing Key)将消息发送到与之完全匹配的队列。只有当消息的路由键与队列绑定时指定的路由键完全相同时,消息才会被路由到队列。这是一种简单的路由策略,适用于点对点通信。
路由键与队列名完全匹配交换机,此种类型交换机,通过RoutingKey路由键将交换机和队列进行绑定, 消息被发送到exchange时,需要根据消息的RoutingKey,来进行匹配,只将消息发送到完全匹配到此RoutingKey的队列。
比如:如果一个队列绑定到交换机要求路由键为"key",则只转发RoutingKey标记为"key"的消息,不会转发"key1",也不会转发"key.1"等等。它是完全匹配、单播的模式
同一个key可以绑定多个queue队列;当匹配到key1时,queue1和queue2都可以收到消息
Topic Exchange(主题交换机)
这种交换机根据消息的路由键与队列绑定时指定的路由键模式(通配符)匹配程度,将消息路由到一个或多个队列。路由键可以使用通配符符号 *(匹配一个单词)和 #(匹配零个或多个单词),允许更灵活的消息路由。用于发布/订阅模式和复杂的消息路由需求。
Topic,主题类型交换机,此种交换机与Direct类似,也是需要通过routingkey路由键进行匹配分发,区别在于Topic可以进行模糊匹配,Direct是完全匹配。
- Topic中,将routingkey通过"."来分为多个部分
- "*":代表一个部分
- "#":代表0个或多个部分(如果绑定的路由键为 "#" 时,则接受所有消息,因为路由键所有都匹配)
然后发送一条信息,routingkey为"key1.key2.key3.key4",那么根据"."将这个路由键分为了4个部分,此条路由键,将会匹配:
- key1.key2.key3.*:成功匹配,因为 * 可以代表一个部分
- key1.# :成功匹配,因为#可以代表0或多个部分
- .key2..key4: 成功匹配,因为第一和第三部分分别为key1和key3,且为4个部分,刚好匹配
- #.key3.key4:成功匹配,#可以代表多个部分,正好匹配中了我们的key1和key2
如果发送消息routingkey为"key1",那么将只能匹配中key1.#,#可以代表0个部分
Headers Exchange(头部交换机)
这种交换机根据消息的标头信息(Headers)来决定消息的路由,而不是使用路由键。队列和交换机之间的绑定规则是根据标头键值对来定义的,只有当消息的标头与绑定规则完全匹配时,消息才会被路由到队列。适用于需要复杂消息匹配的场景。
headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了
消费方指定的headers中必须包含一个"x-match"的键。键"x-match"的值有2个:
- x-match = all :表示所有的键值对都匹配才能接受到消息
- x-match = any :表示只要有键值对匹配就能接受到消息
发送消息时间,如果其他参数信息是{ "name":"xiaomingXX", "sex":"男" },因为queue2的x-match是any,只需要有一个键值对匹配所以就能接收到消息,所以queue2可以接收到消息;queue1的x-match是all,需要所有的键值对都匹配才能接收到消息,所以此时queue1接收不到消息
Fanout Exchange(广播交换机)
这种交换机将消息广播到与之绑定的所有队列,无论消息的路由键是什么。用于发布/订阅模式,其中一个消息被广播给所有订阅者。
Fanout,广播类型交换机,此种交换机,会将消息分发给所有绑定了此交换机的队列,此时RoutingKey参数无效。
fanout类型交换机下发送消息一条,无论RoutingKey是什么,queue1,queue2,queue3,queue4都可以收到消息
Default Exchange(默认交换机)
这是 RabbitMQ 默认实现的一种交换机,它不需要手动创建。当消息发布到默认交换机时,路由键会被解释为队列的名称,消息会被路由到与路由键名称相同的队列。默认交换机通常用于点对点通信,但不支持复杂的路由策略。
这些不同类型的交换机允许你在 RabbitMQ 中实现各种不同的消息路由和分发策略,以满足不同的应用需求。选择适当的交换机类型对于有效的消息传递非常重要。
RabbitMQ 如何保证消息不丢失/消息持久化?
根据上图我们能知道整个流程中可能会出现三种消息丢失场景:
● 生产者发送消息到 RabbitMQ 服务器的过程中出现消息丢失。 可能是网络波动未收到消息,又或者是服务器宕机。
● RabbitMQ 服务器消息持久化出现消息丢失。 消息发送到 RabbitMQ 之后,未能及时存储完成持久化,RabbitMQ 服务器出现宕机重启,消息出现丢失。
● 消费者拉取消息过程以及拿到消息后出现消息丢失。 消费者从 RabbitMQ 服务器获取到消息过程出现网络波动等问题可能出现消息丢失;消费者拿到消息后但是消费者未能正常消费,导致丢失,可能是消费者出现处理异常又或者是消费者宕机。
针对上述三种消息丢失场景,RabbitMQ 提供了相应的解决方案,confirm
消息确认机制(生产者),消息持久化
机制(RabbitMQ 服务),ACK 事务
机制(消费者)
针对生产者
confirm事务处理机制
RabbitMQ 提供了confirm
事务处理机制,允许生产者在发送消息时将操作包装在一个事务中,以确保消息的可靠性传递。在 RabbitMQ 中,事务是通过通道(Channel)来实现的。可以通过以下步骤进行事务处理:
- 开启事务:在生产者端,可以通过调用 Channel 的 tx_select 方法来开启一个事务。这将启动一个新的事务,并将所有后续的消息发布操作放在该事务内。
- 发送消息:接下来在事务中,可以正常发送消息。如果消息发送失败,事务会自动回滚。
- 提交事务:如果事务中所有消息发送成功后,需要提交事务。可以通过调用 Channel 的tx_commit方法提交事务。
- 处理异常:如果在事务过程中发生异常,可以使用 try/catch 快来捕获异常。然后在异常处理过程中,调用 Channel 的 tx_rollback 方法来回滚 RabbitMQ 相关的事务操作。
需要注意的是,RabbitMQ 的事务处理是基于存储过程的,它可以保证在事务中的操作要么全部成功,要么全部失败。但是,由于 RabbitMQ 是一个异步的消息队列系统,事务处理可能会对其性能产生影响。因此,需要根据具体的应用场景和需求来权衡是否需要使用事务以及如何使用事务。
如何使用confirm机制
在生产者开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq之中,rabbitmq会给你回传一个ack
消息,告诉你这个消息发送OK了;如果rabbitmq没能处理这个消息,会回调你一个nack
接口,告诉你这个消息失败了,你可以进行重试。而且你可以结合这个机制知道自己在内存里维护每个消息的id,如果超过一定时间还没接收到这个消息的回调,那么你可以进行重发。
Java
// 开启confirm
channel.confirm();
//发送成功回调
public void ack(String messageId){
}
// 发送失败回调
public void nack(String messageId){
// 重发该消息
}
针对RabbitMQ
消息持久化
RabbitMQ 允许消息的持久化,以确保即使在 RabbitMQ 服务器重新启动后,消息也不会丢失。
RabbitMQ 可以通过以下方式实现消息的持久化:
- 消息持久化:在 RabbitMQ 中,只需要在发送消息时,将delivery_mode属性设置为 2,就可以将消息标记为持久化。
- 队列持久化:在 RabbitMQ 中声明队列时,也可以将队列声明为持久化。RabbitMQ 中的队列分为三种不同类型经典队列,仲裁队列和流式队列。其中,经典队列需要将durable属性设置为true。而仲裁队列和流式队列默认必须持久化保存。
- 交换机持久化:与经典队列类似,RabbitMQ 也可以在声明交换机时,将交换机的 durable 属性设置为true,这样就可以将交换机标记为持久化。
集群模式
先来介绍下RabbitMQ三种部署模式:
- 单节点模式:最简单的情况,非集群模式,节点挂了,消息就不能用了。业务可能瘫痪,只能等待。
- 普通模式:消息只会存在与当前节点中,并不会同步到其他节点,当前节点宕机,有影响的业务会瘫痪,只能等待节点恢复重启可用(必须持久化消息情况下)。
- 镜像模式:消息会同步到其他节点上,可以设置同步的节点个数,但吞吐量会下降。属于RabbitMQ的HA方案
为什么设置镜像模式集群,因为队列的内容仅仅存在某一个节点上面,不会存在所有节点上面,所有节点仅仅存放消息结构和元数据。
补偿机制
为什么还要消息补偿机制呢?虽然以上方案,基本可以保证消息的高可用不丢失的问题。
但是作为有追求的程序员来讲,要绝对保证我的系统的稳定性,有一种危机意识。
比如:持久化的消息,保存到硬盘过程中,当前队列节点挂了,存储节点硬盘又坏了,消息丢了,怎么办?
1)生产端首先将业务数据以及消息数据入库,需要在同一个事务中,消息数据入库失败,则整体回滚。
2)根据消息表中消息状态,失败则进行消息补偿措施,重新发送消息处理。
针对消费者
ACK确认机制
多个消费者同时收取消息,比如消息接收到一半的时候,一个消费者死掉了(逻辑复杂时间太长,超时了或者消费被停机或者网络断开链接),如何保证消息不丢?
使用rabbitmq提供的ack机制,服务端首先关闭rabbitmq的自动ack,然后每次在确保处理完这个消息之后,在代码里手动调用ack。这样就可以避免消息还没有处理完就ack。才把消息从内存删除。
这样就解决了,即使一个消费者出了问题,但不会同步消息给服务端,会有其他的消费端去消费,保证了消息不丢的case。
总结
如果需要保证消息在整条链路中不丢失,那就需要生产端、mq自身与消费端共同去保障。
生产者 :对生产的消息进行状态标记,开启confirm
机制,依据mq的响应来更新消息状态,使用定时任务重新投递超时的消息,多次投递失败进行报警。
RabbitMQ :开启持久化
,并在落盘后再进行ack。如果是镜像部署模式,需要在同步到多个副本之后再进行ack。
消费者 :开启手动ack
模式,在业务处理完成后再进行ack,并且需要保证幂等。
通过以上的处理,理论上不存在消息丢失的情况,但是系统的吞吐量以及性能有所下降。在实际开发中,需要考虑消息丢失的影响程度,来做出对可靠性以及性能之间的权衡。
RabbitMQ中如何解决消息堆积问题?
解决方案
- 消费者处理消息的速度太慢
○ 增加消费者数量:通过水平扩展,增加消费者的数量来提高处理能力。
○ 优化消费者性能:提高消费者处理消息的效率,例如优化代码、增加资源。
○ 消息预取限制(prefetch count):调整消费者的预取数量以避免一次处理过多消息而导致处理缓慢。 - 队列的容量太小
○ 增加队列的容量:调整队列设置以允许更多消息存储。 - 网络故障
○ 监控和告警:通过监控网络状况并设置告警,确保在网络故障时快速发现并解决问题。
○ 持久化和高可用性:确保消息和队列的持久化以避免消息丢失,并使用镜像队列提高可用性。 - 消费者故障
○ 使用死信队列:将无法处理的消息转移到死信队列,防止堵塞主队列。
○ 容错机制:实现消费者的自动重启和错误处理逻辑。 - 队列配置不当
○ 优化队列配置:检查并优化消息确认模式、队列长度限制和其他相关配置。 - 消息大小
○ 消息分片:将大型消息分割成小的消息片段,加快处理速度。 - 业务逻辑复杂或耗时
○ 优化业务逻辑:简化消费者中的业务逻辑,减少处理每个消息所需的时间。 - 消息产生速度快于消费速度
○ 使用消息限流:控制消息的生产速度,确保它不会超过消费者的处理能力。
○ 负载均衡:确保消息在消费者之间公平分配,避免个别消费者过载。 - 其他配置优化
○ 消息优先级:使用消息优先级确保高优先级消息优先处理。
○ 调整RabbitMQ配置:优化RabbitMQ服务的配置,如文件描述符限制、内存使用限制等。
大量消息在MQ中积压的解决方案
- 先修复consumer的问题,确保其恢复消费速度,然后将现有consumer都停掉;
- 新建⼀个topic,partition是原来的10倍,临时建⽴好原先10倍或者20倍的queue数量;
- 然后写⼀个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据;消费之后不做耗时的处理,直接均匀轮询写⼊临时建⽴好的10倍数量的queue;
- 接着临时征⽤10倍的机器来部署consumer,每⼀批consumer消费⼀个临时queue的数据;
- 这种做法相当于是临时将queue资源和consumer资源扩⼤10倍,以正常的10倍速度来消费数据;
- 等快速消费完积压数据之后,得恢复原先部署架构,重新⽤原先的consumer机器来消费消息。
总结
- 修复并停掉consumer;
- 新建⼀个topic,partition是原来的10倍,建⽴临时queue,数量是原来的10倍或20倍;
- 写临时consumer程序,临时征⽤10倍的机器去消费数据;
- 消费完成之后,恢复原先consumer;
RabbitMQ中如何保证消息不被重复消费?
什么情况会导致消息被重复消费呢?
- 生产者:生产者可能会重复推送一条数据到 MQ 中,比如 Controller 接口被重复调用了 2 次,没有做接口幂等性导致的;
- RabbitMQ:在消费者消费完准备响应 ack 消息消费成功时,MQ 突然挂了,导致 MQ 以为消费者还未消费该条数据,MQ 恢复后再次推送了该条消息,导致了重复消费。
- 消费者:消费者已经消费完消息,正准备但是还未响应给ack消息到时,此时消费者挂了,服务重启后 MQ 以为消费者还没有消费该消息,再次推送了该条消息。
解决方案
- 使用数据库唯一键约束
缺点:局限性很大,仅仅只能用在我们数据新增场景,并且性能也比较低 - 使用乐观锁
假设是更新订单状态,在发送的消息的时候带上修改字段的版本号
缺点:如果说更新字段比较多,并且更新场景比较多,可能会导致数据库字段增加并且还有可能出现多条消息同时在队列中此时他们修改字段版本号一致,排在后续的消息无法被消费 - 简单的消息去重,插入消费记录,增加数据库判断
优点:很多场景下的确能起到不错的效果
缺点:
- 这个消费者的代码执行需要1秒,重复消息在执行期间(假设100毫秒)内到达(例如生产者快速重发,Broker重启等),增加校验的地方是不是还是没数据(因为上一条消息还没消费完,没有记录)
- 那么就会穿透掉检查的挡板,最后导致重复的消息消费逻辑进入到非幂等安全的业务代码中,从而引发重复消费的问题
- 并发消息去重基于消息幂等表
● 缺点:如果说第一次消息投递异常没有消费成功,并且没有将消息状态给置为成功或者没有删除消息表记录,此时延时消费每次执行下列都是一直处于消费中,最后消费就会被视为消费失败而被投递到死信Topic中
● 方案:插入的消息表必须要带一个最长消费过期时间,例如10分钟
● 上述方案只需要一个存储的中心媒介,那我们可以选择更灵活的存储中心媒介,比如Redis。使用Redis有两个好处:
○ 性能上损耗更低
○ 上面我们讲到的超时时间可以直接利用Redis本身的ttl实现
总结
- 利用数据库唯一键约束
- 可以利用我们的乐观锁
- 插入消费记录
不丢和不重是矛盾的(在分布式场景下),总的来说,开发者根据业务的实际需求来选择相应的方式即可。