消息队列之RabbitMQ

RabbitMQ

RabbitMQ安装(待完善)

RabbitMQ工作机制

RabbitMQ基本的流程就是生产者生产消息投递到RabbitMQ,再由RabbMQ投递到消费者。实际就是生产者消费者模式,只不过多了RabbitMQ这一层。

接下来认识下RabbitMQ中的基本概念:

  • ConnectionFactory(连接管理器):应用程序与Rabbit之间建立连接的管理器,程序代码中使用;
  • Channel(信道):消息推送使用的通道;
  • Exchange(交换器):用于接受、分配消息;
  • Queue(队列):用于存储生产者的消息;
  • RoutingKey(路由键)
  • BindingKey(绑定键)

ce9184a28693480382b0e76eb1ff625f.png

通过图可以看到:

生产者通过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代码如下:
image

2). 保证消费者宕机后消息不丢失

RabbitMQ默认采用自动ACK的机制,意思就是消费者收到一个消息时,RabbitMQ就会自动把这条消息删除
因此可以开启手动ack,在finally代码块确认ack,以确保消息是被消费可删除。
如果消费者还没消费就宕机,MQ就会感知到并且重新发送这条消息给其他消费者。(MQ是如何感知消费者宕机的呢)

Demo代码如下:
image
image

同时消息消费失败时,可标记消息为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 来处理。

一句话,单个队列对应单个消费者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值