RabbitMQ
RabbitMQ安装(待完善)
RabbitMQ工作机制
RabbitMQ基本的流程就是生产者生产消息投递到RabbitMQ,再由RabbMQ投递到消费者。实际就是生产者消费者模式,只不过多了RabbitMQ这一层。
接下来认识下RabbitMQ中的基本概念:
- ConnectionFactory(连接管理器):应用程序与Rabbit之间建立连接的管理器,程序代码中使用;
- Channel(信道):消息推送使用的通道;
- Exchange(交换器):用于接受、分配消息;
- Queue(队列):用于存储生产者的消息;
- RoutingKey(路由键)
- BindingKey(绑定键)
通过图可以看到:
生产者通过ConnectionFactory与RabbitMQ建立连接,接着使用channel发送消息到Exchange上。
消息是存储在队列中的,消息者通过监听队列,队列就能push消息到消费者。
需要注意的是,一个队列哪怕有多个消费者,它的一条消息也只会发给其中的一个消费者。
那么如何决定消息是投递到哪个队列里的呢?
首先在exchange中会与queue进行绑定,并指定BindingKey
,然后生产者在向指定的exchange发送消息时,也会带上一个key,叫RoutingKey,exchange就会把消息放到RoutingKey等于BindingKey的队列中去。
exchange有多种类型,不同类型的exchange区别就在于RoutingKey与BindingKey的匹配规则,前面我们说的属于directExchange,它需要两个key完全匹配上,才会把消息投递
到队列。
需要注意的是,如果消息发送到exchange,发现key没有跟任何queue绑定,exchange会丢弃消息。
RabbitMQ的使用场景
也就是为什么在项目中使用消息队列,可以结合自身项目来谈,主要有以下几个应用:
- 复杂系统的解耦
- 复杂链路的异步调用
- 瞬时高峰的削峰处理
如何保证全链路消息不丢失
首先要搞清楚消息丢失可能会出现在什么环节:
- 生产者投递到消息队列,因网络原因投递失败;
- 消息队列服务宕机;
- 消费者消费信息失败;
1). 保证生产者投递到消息队列不丢失
采用confirm机制–生产端采用confirm模式之后投递消息到rabbitmq,rabbitmq一旦将信息持久化到磁盘之后,就会发送一个confirm消息给生产者;
如此一来,生产者收到消息后就能知道消息已被成功接收,若没有收到消息就能知道这条消息丢失了,可以采取措施重新投递。
Demo代码如下:
2). 保证消费者宕机后消息不丢失
RabbitMQ默认采用自动ACK的机制,意思就是消费者收到一个消息时,RabbitMQ就会自动把这条消息删除
因此可以开启手动ack,在finally代码块确认ack,以确保消息是被消费可删除。
如果消费者还没消费就宕机,MQ就会感知到并且重新发送这条消息给其他消费者。(MQ是如何感知消费者宕机的呢)
Demo代码如下:
同时消息消费失败时,可标记消息为nack,代表消费失败,MQ就是重发这条消息
比如可以在catch代码块中加入如下代码
channel.basicNack(
delivery.getEnvelope().getDeliveryTag(),
true);
注意上面第二个参数是true,意思就是让RabbitMQ把这条消息重新投递给其他的服务实例,因为自己没处理成功。
3). 保证MQ宕机消息不丢失
主要有三部分:队列的持久化、消息的持久化和exchange持久化
队列的持久化只需要在声明队列时,把durable属性设为true即可。
但这是还不够的,如果此时MQ挂了,重启后队列会还在,可队列中的消息还会丢失,因此我们也要设置消息的持久化。
具体代码就是在发送消息时:
// 设置消息持久化
channel.basicPublish(exchangeName, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, messag);
我们再来看下发送消息方法的各个参数
void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;
其中持久化发送要把props设置为MessageProperties.PERSISTENT_TEXT_PLAIN 。
上面阐述了队列的持久化和消息的持久化,如果不设置exchange的持久化对消息的可靠性来说没有什么影响,但是同样如果exchange不设置持久化,那么当broker服务重启之后,exchange将不复存在,那么既而发送方rabbitmq producer就无法正常发送消息。
引入消息中间件之后带来的问题
1). 整体系统的可用性降低
项目引入了MQ,就导致多了一个依赖。一旦多了一个依赖,就会导致可用性降低。因此需要考虑RabbitMQ宕机之后的高可用技术兜底方案。
2). 系统的稳定性降低
MQ可能会出现发送重复消息等等的情况,导致脏数据的产生故障会增多,各种各样乱七八糟的问题都可能产生。
3). 分布式一致性问题
举个例子,比如说系统C现在处理自己本地数据库成功了,然后发送了一个消息给MQ,系统D也确实是消费到了。但是结果不幸的是,系统D操作自己本地数据库失败了,那这个时候咋办?系统C成功了,系统D失败了,会导致系统整体数据不一致了啊。
所以此时又需要使用可靠消息最终一致性的分布式事务方案来保障。
unack消息积压的问题
对每个channel(其实对应了一个消费者服务实例,大体可以这么来认为),RabbitMQ投递消息的时候,都是会带上本次消息投递的一个delivery tag的,唯一标识一次消息投递。
然后,我们进行ack时,也会带上这个delivery tag,基于同一个channel进行ack,ack消息里会带上delivery tag让RabbitMQ知道是对哪一次消息投递进行了ack,此时就可以对那条消息进行删除了。
对于一个channel来说,不论是自动ack还是手动ack,在每个时段都总有存在unack的消息,比如
- MQ推送消息的时候
- 手动ack时,消费者还未消费成功的时候
- 手动ack即使确认ack之后,也是异步执行,不会立马ack
因此就会存在一个unack消息积压的问题,如果消息过多就会导致消费者服务内存飙升,最终OOM。
那么如何解决呢?RabbitMQ基于一个prefetch count来控制这个unack message的数量。
你可以通过如下代码设置:
// 10就表示消费者unack消息的数量不会超过十
channel.basicQos(10)
同时要对prefetch进行合理设置。官方推荐是100~300,过小会导致性能降低,过大有内存溢出的风险。
RabbitMQ集群
1). 为什么要使用集群
- 保证当RabbitMQ一个节点,生产者消费者还能正常运作,实现高可用行
- 通过增加节点来扩展rabbitMQ的消息处理能力
2). RabbitMQ集群的特性
RabbitMQ的集群是由多个节点组成的,但我们发现不是每个节点都有所有队列的完全拷贝。
RabbitMQ节点不完全拷贝特性
为什么默认情况下RabbitMQ不将所有队列内容和状态复制到所有节点?
两个原因:
- 存储空间——如果每个节点都拥有所有队列的完全拷贝,这样新增节点不但没有新增存储空间,反而增加了更多的冗余数据。
- 性能——如果消息的发布需安全拷贝到每一个集群节点,那么新增节点对网络和磁盘负载都会有增加,这样违背了建立集群的初衷,新增节点并没有提升处理消息的能力,最多是保持和单节点相同的性能甚至是更糟。
3). RabbitMQ集群的特性带来的问题
RabbitMQ节点具有不完全拷贝特性,这就意味着一旦相关的节点崩溃了,与该节点队列相关的绑定与消息消息者就无法获取。
那么要如何解决这个问题呢?要分两种情况来看:
- 消息已经进行了持久化,那么当节点恢复,消息也恢复了;
- 消息未持久化,可以使用下文要介绍的双活冗余队列,镜像队列保证消息的可靠性;
镜像队列的作用
RabbitMQ默认的集群模式,并不包括队列的高可用,队列节点宕机直接导致该队列无法应用,只能守候重启,所以要想在队列节点宕机或故障也能正常应用,就要复制队列内容到集群里的每个节点,须要创建镜像队列.
每一个queue都会创建在一个master node或者 多个slaves node上 ,并且当master 丢失的时候,最老的slaves node将会变成最新的master,当然这有一个前提那就是要求slave node必须已经同步了master node的内容,如果没有同步的话那么这个slave node是不可以成为master node的
镜像队列消息同步
配置镜像队列时有一个属性ha-sync-mode,支持两种模式 automatic 或 manually 默认为 manually。
当 ha-sync-mode = manually,新节点加入到镜像队列组后,可以从左节点获取当前正在广播的消息,但是在加入之前已经广播的消息无法获取,所以会处于镜像队列之间数据不一致的情况,直到加入之前的消息都被消费后,主从镜像队列数据保持一致。当加入之前的消息未全部消费完之前,主节点宕机,新节点选为主节点时,这部分消息将丢失。
当 ha-sync-mode = automatic,新加入组群的 Slave 节点会自动进行消息同步,使主从镜像队列数据保持一致。
4). 集群的节点类型
集群节点存储类型分为两种:
- 内存节点
- 磁盘节点
磁盘节点就是将配置信息和元数据存储在磁盘上,内存节点就是存储在内存上,显然内存节点的速度更快。
单节点必须是磁盘类型的,不然重启后RabbitMQ的配置就会消失。
RabbitMQ要求集群中至少有一个磁盘节点,当节点加入和离开集群时,必须通知磁盘节点。
如果集群中的唯一一个磁盘节点,结果这个磁盘节点还崩溃了,那会发生什么情况?集群还能够正常运行,但你不能更改任何东西,比如声明队列、添加用户、更改权限等。
如何处理消息重复
这个问题实际上就是如果消费端收到两条一样的消息,应该怎样处理?
需要知道的是RabbitMQ 不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重,也就是消费端处理消息的业务逻辑保持幂等性。
幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。
因此这个需要结合具体业务去谈:
- 如果是新增一个数据到数据库,可以根据主键去查询下或通过唯一键约束,如果存在就使用update。
- 如果是存入Redis,直接使用set就行,天然幂等性
如何保证消息的有序性
通常mq可以保证先到队列的消息按照顺序分发给消费者消费来保证顺序,但是一个队列有多个消费者消费的时候,那将失去这个保证,因为这些消息被多个线程并发的消费。
要解决的思路就是:
-
拆分多个 queue,每个 queue 一个 consumer;
-
一个 queue 对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
一句话,单个队列对应单个消费者。