简介
RabbitMQ
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(称面向消息的中间件)。RabbitMQ服务器是使用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端。
RabbitMQ是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑RabbitMQ是一个快递站,一个快递员帮你传递快件。RabbitMQ与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据
什么是AMQP协议?
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
RabbitMQ的架构
名词介绍
**Broker(中间件-**消息队列服务器实体**)**:接收和分发消息的应用,RabbitMQ Server就是Message Broker
**Connection**: publisher / consumer和 broker之间的一个TCP连接
**Channel(通道)**:如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id 帮助客户端和message broker识别 channel,所以channel 之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建TCP connection的开销
**Exchange(**消息交换机**)**: message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。
Exchange是接受生产者消息并将消息路由到消息队列的关键组件
常用的类型有:
direct (point-to-point), topic(publish-subscribe) and fanout (multicast)
**Queue(消息队列载体)每个消息都会被投入到一个或多个队列
Routing Key((Exchange)交换机的路由规则) :生产者将消息发送到交换机时会携带一个key,来指定路由规则
** binding Key(绑定) :在绑定Exchange和Queue时,会指定一个BindingKey,生产者发送消息携带的RoutingKey会和bindingKey对比,若一致就将消息分发至这个队列
vHost 虚拟主机:每一个RabbitMQ服务器可以开设多个虚拟主机每一个vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的 "交换机exchange、绑定Binding、队列Queue",更重要的是每一个vhost拥有独立的权限机制,这样就能安全地使用一个RabbitMQ服务器来服务多个应用程序,其中每个vhost服务一个应用程序。
交换机类型
1.direct Exchange(直接交换机)
匹配路由键,只有完全匹配消息才会被转发
2.Fanout Excange(扇形交换机)
将消息发送至所有的队列
3.Topic Exchange(主题交换机)
将路由按模式匹配,此时队列需要绑定要一个模式上。符号"#"匹配一个或多个词,"* "符号"匹配不多不少一个词。因此"abc.#"能够匹配到"abc.def.ghi",但是"abc." 只会匹配到"abc.def"。
4.Header Exchange
在绑定Exchange和Queue的时候指定一组键值对,header为键,根据请求消息中携带的header进行路由
工作模式
1.simple (简单模式)
一个消费者消费一个生产者生产的信息
2.Work queues(工作模式)
一个生产者生产信息,多个消费者进行消费,但是一条消息只能消费一次
3.Publish/Subscribe(发布订阅模式)
生产者首先投递消息到交换机,订阅了这个交换机的队列就会收到生产者投递的消息
4.Routing(路由模式)
生产者生产消息投递到direct交换机中,扇出交换机会根据消息携带的routing Key匹配相应的队列
5.Topics(主题模式)
生产者生产消息投递到topic交换机中,上面是完全匹配路由键,而主题模式是模糊匹配,只要有合适规则的路由就会投递给消费者
保证消息的稳定性
1.消息持久化
RabbitMQ的消息默认存在内存中的,一旦服务器意外挂掉,消息就会丢失
消息持久化需做到三点
- Exchange设置持久化
- Queue设置持久化
- Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息
2.ACK确认机制
多个消费者同时收取消息,收取消息到一半,突然某个消费者挂掉,要保证此条消息不丢失,就需要acknowledgement机制,就是消费者消费完要通知服务端,服务端才将数据删除
这样就解决了,及时一个消费者出了问题,没有同步消息给服务端,还有其他的消费端去消费,保证了消息不丢的case。
3.设置集群镜像模式
RabbitMQ三种部署模式:
- 单节点模式:最简单的情况,非集群模式,节点挂了,消息就不能用了。业务可能瘫痪,只能等待。
- 普通模式:默认的集群模式,某个节点挂了,该节点上的消息不能用,有影响的业务瘫痪,只能等待节点恢复重启可用(必须持久化消息情况下)。
- 镜像模式:把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的HA方案
为什么设置镜像模式集群,因为队列的内容仅仅存在某一个节点上面,不会存在所有节点上面,所有节点仅仅存放消息结构和元数据。下面自己画了一张图介绍普通集群丢失消息情况
4.消息补偿机制
持久化的消息,保存到硬盘过程中,当前队列节点挂了,存储节点硬盘又坏了,消息丢了,怎么办?
产线网络环境太复杂,所以不知数太多,消息补偿机制需要建立在消息要写入DB日志,发送日志,接受日志,两者的状态必须记录。
然后根据DB日志记录check 消息发送消费是否成功,不成功,进行消息补偿措施,重新发送消息处理。
如何实现延迟队列
RabbitMQ本身没有延迟队列,需要靠TTL和DLX模拟出延迟的效果
TTL(Time To Live)
RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter
RabbitMQ针对队列中的消息过期时间有两种方法可以设置。
A: 通过队列属性设置,队列中所有消息都有相同的过期时间。
B: 对消息进行单独设置,每条消息TTL可以不同。
如果同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就成为dead letter
DLX (Dead-Letter-Exchange)
RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由。
x-dead-letter-exchange :出现dead letter之后将dead letter重新发送到指定exchange
x-dead-letter-routing-key :指定routing-key发送
队列出现dead letter的情况有:
- 消息或者队列的TTL过期
- 队列达到最大长度
- 消息被消费端拒绝(basic.reject or basic.nack)并且requeue=false
利用DLX,当消息在一个队列中变成死信后,它能被重新publish到另一个Exchange。这时候消息就可以重新被消费。
消息队列特点-优缺点
优点:
解耦、异步、削峰
缺点:
系统可用性降低
系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,人 ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整,MQ 一挂,整套系统崩溃的,你不就完了?如何保证消息队列的高可用,可以点击这里查看。
系统复杂度提高
硬生生加个 MQ 进来,你怎么[保证消息没有重复消费]?怎么[处理消息丢失的情况]?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。
一致性问题
A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
高可用集群架构
非常经典的 mirror镜像模式,保证 100%数据不丢失。在实际工作中也是用得最多的,并且实现非常的简单,一般互联网大厂都会构建这种镜像集群模式。
mirror镜像队列,目的是为了保证 rabbitMQ数据的高可靠性解决方案,主要就是实现数据的同步,一般来讲是2 - 3个节点实现数据同步。对于100%数据可靠性解决方案,一般是采用 3个节点。
问答:
如何避免消息堆积
RabbitMQ丢消息怎么办?
按照"生产者 -> 交换器 -> 队列 -> 消费者"的模式,一旦发生丢消息的情况,无非有三种情况:生产者丢消息、消息队列丢消息、消费者丢消息。下面我们逐个进行分析:
1、对于生产者丢消息,RabbitMQ提供的transaction和confirm机制可以保证生产者不丢消息,transaction机制类似数据库的事务,只有当消息发送成功,事物才会被提交,否则事务被被回滚。因为每次发消息都必须开启事物,所以transaction机制会导致RabbitMQ吞吐量降低,一般建议使用confirm机制,即消息被正确投递则发送ACK给生产者,否则发送NACK给生产者。
2、对于消息队列丢消息,解决方案我们在前面有提到过,主要有两点,第一,声明队列的时候设置durable为true,这表示这是一个支持持久化的队列。第二,发送消息的时候,设置DeliveryMode为2,这表示消息支持持久化的磁盘,如果有一天RabbitMQ遭遇不幸,消息会被持久化到磁盘上,所以说,习惯性保存是个好习惯啊......
3、对于消费者丢消息,解决方案是手动ACK,因为只有队列收到ACK时,它才会从队列中删除这条消息,否则,这条消息会重新回到队列中,只要它能重新回到队列、重新处理,它怎么会丢呢
防止消息丢失:
当队列中的消息一直失败,如何删除队列中的这条消息
页面操作:点击这个队列名称->点击消息->删除消息
消息队列配置项已经修改exchange和队列的绑定规则,但是依然会匹配之前的规则
页面查看队列的绑定规则,存在持久化问题
将交换器、队列和消息都设置持久化之后就能保证数据不会被丢失吗?当然不是,多个方面
- 消费者端: 消费者订阅队列将autoAck设置为true,虽然消费者接收到了消息,但是没有来得及处理就宕机了,那数据也会丢失,解决方案就是以为手动确认接收消息,待处理完消息之后,手动删除消息
- 在rabbitmq服务端,如果消息正确被发送,但是rabbitmq未来得及持久化,没有将数据写入磁盘,服务异常而导致数据丢失,解决方案,可以通过rabbitmq集群的方式实现消息中间件的高可用性
RaabitMQ的死信机制
从实用性的角度来讲,它可以帮助我们实现"延时队列",虽然在更多的场景下,我们希望消息能被立即处理,因为这样看起来更像一个"实时"的行为。可在实际应用过程中,我们难免会遇到这样一种情况,一条消息经过手动ACK以后从队列中移除,结果消费者端问你能不能再消费一次这条消息,所以,Kafka里就提供了两种策略,即最多一次和至少一次,最多一次保证的是消息不会被重复消费,而至少一次保证的是消息100%被成功消费。所以,简单来说,在为RabbitMQ配置了死信的情况下,可以让部分消息有机会重新进入队列、重新被消费。那么,什么情况下会产生死信呢?主要有下面三种情况:
- 消息被否定确认,使用channel.basicNack或channel.basicReject,并且此时requeue属性被设置为false。
- 消息在队列的存活时间超过设置的TTL时间。
- 消息队列的消息数量已经超过最大队列长度。
接下来,为了配合死信机制,我们必须要声明死信队列,建议为每一个需要配置死信的事件单独定义一个死信队列,
RabbitMQ重试与超时
利用消息/队列的TTL实现超时,利用死信实现重试,消息TTL和队列TTL的不同在于,一个队列超时则队列内的消息会被全部清空,而一个消息超时则可以在清空前决定是否要清空。
重试与超时最大的问题其实在于幂等性,因为在以往的实践中,当我们的消费者变成一个第三方的API接口的时候,我们很难知道,一个消息到底需要处理多久,我一直不明白,为什么宝洁这样的公司,它一个API接口居然能等将近30分钟,而更加令人难以忍受的,是大量只能调用一次的接口,这类接口既无法保证能100%调用成功,同样无法保证,第二次调和第一次调效果完全一样,所以,关于重试与超时这部分,其实应该结合实际业务去设计,因为每个人的诉求可能都不一样。
参考
RabbitMQ参考地址
RabbitMQ基础了解参考-写的很好很全面(重点看 )
https://blog.csdn.net/qinyuanpei/article/details/108224978-示例