RabbitMQ是如何确保消息的可靠性的?

一、消息发送流程

首先说一下消息队列发送消息的整个流程,就是由消息的生产者先发送消息到交换机,然后再由交换机发送到队列,然后再由队列发送到消息的消费者。经过这个流程,也就是说他可能会出现问题的地方,首先是由消息的生产者到交换机这里,然后就是交换机到队列这个位置,还有就是由消息消费者自己处理的时候也可能会出现错误。只要在这三个部分保证了每一次消息的流转是可靠的,那么也就基本实现了消息队列处理消息时候的可靠性。

二、发送者的可靠性:

消息生产者到交换机,也就是发送者的可靠性

①生产者重试机制,就是消息队列连接的时候出现了网络故障导致他连接中断,消息就发不出去了。这样的话就可以开启一下这个生产者重试机制,就是生产者与消息队列连接失败的时候,可以去尝试多次连接。其实我们一般就不建议去开启这个机制,因为他是一个阻塞机制,也就是说你重试的时候整个线程都处在阻塞状态。但其实消息队列一般是在内网运行,也就是说服务端调用服务端一般都是一个集群运行的,网络问题基本是不可能出现的。

②生产者的确认机制,一般情况下,生产者与消息队列连上之后基本上不会发生消息丢失的情况。但是也有一些比较奇怪的情况,比如说,生产者就是发送消息,但是他并没有找到交换机。或者是找到交换机后,又没有找到合适的,就是没有订阅这个交换机的队列,所以就没有办法把消息一直传递下去。这样的话,我们可以开启一个生产者的确认机制,就是这个消息发出去之后,到哪里了你给我一个回复。比如说我们有Publisher Return,就是当你投递到消息队列后,但是路由失败的时候,也就是由交换机发送到队列的时候失败了,我们可以通过这个Publisher Return返回一个异常信息。还有一个就是Publisher Confirm这个是由我们的消费生产者到交换机,这一部分如果是出现了错误的时候,也是可以返回一个标识。两种机制默认都是关闭状态,但是我们开启的时候只要通过配置文件去打开它就完事了。但是生产者确认机制其实我们是不经常使用的,因为他比较消耗消息队列的性能,因为他就是你发消息之后他要给你检查,然后检查完了还要给你回复。就导致性能会比较低。消息队列是相对来说还是比较可靠的,比如说我们消息发送到交换机的时候出现错误,一般都是因为我们把这个交换机的名字写错了,导致他找不到这个交换机。其实这种错误我们很容易就排查出来了。或者是交换机到队列的时候,如果没发出去,就可能因为你这个订阅的key写错了,导致他发不出去了。这种错误我们非常容易检查出来,所以说生产者消息确认机制是不经常开启的。

三、消息内部流转的可靠性

第二个阶段,也就是由消息队列在内部处理消息的时候,可能也会出现问题。我们解决办法有:

①消息持久化,其实消息队列他是默认消息持久化的。他为什么要做消息持久化,就是说消息队列的数据是在内存中存储的,如果哪一天网络断了,然后你重新连了之后,消息就没有了。但是消息持久化之后,可以保证重连之后这个消息还在队列中,还能被路由出去。

②lazy队列,也就是惰性队列,就是接收的消息保存在内存中,可以降低收发的延迟。但是某些特殊情况下,比如说生产者生产的消息突然就增多了,然后超过了消费者消费的速度。这样的话,有大量的数据都存在了内存中,这就是消息堆积问题。消息堆积在内存会越来越多。对于这种情况,消息队列的处理机制,一般就是内存如果是满了的话,他们就把这些消息存到磁盘上。他这个行为是会浪费一定时间的,并且会阻塞消息路由的进程,就是在把消息从内存刷到磁盘上的这段时间内,他整个消息队列是不会处理新的消息的。

为了解决这个问题,比如说rabbitMQ他增加了一个叫惰性队列的一种模式。收到的消息会直接写入磁盘,而非内存,也因此很难触发 mq 的内存预警,几乎不会出现刷盘的情况。消费者消费惰性队列的消息,也需要先从磁盘中读取并加载到内存中,也就是一个懒加载,实际上这也会使延迟稍微高一点,毕竟磁盘的性能和内存还是有很大差距的,但是也在可以接收到范围内。支持数百万的消息存储,这也是因为他是磁盘存储空间非常大。rabbitMQ在3.12版本之后,LazyQueue已经成为所有队列的默认格式。

四、消费者的可靠性

也就是消费者消费消息的可靠性。也有一个消费者确认机制,也就是说当他消费消费结束后,会发给消息队列一个回执,就是告诉消息处理的状态。

比如说处理成功回复ask,处理失败回复nask,还有就是处理失败并拒绝再接收这个消息回复reject,拒绝消息后,消息队列就不会重新发这个消息了,而是把这个消息从队列里删除。所以reject这个回复其实就使用的比较少了。

springAMQP组件帮我们实现了这种消费者确认机制,就是把这个机制融合到了他的框架里边,然后我们使用起来非常简单。只要配置一下这个消费者确认机制就完事了,一般的话,我们配成自动模式。就是业务有异常的话就自动返回nask,如果消息处理或者是校验异常会返回这个reject。然后正常的话就返回ask。

但是这个机制是有一个缺陷的,就是如果触发消息重试,我们不去进行判断的话,消费失败之后我们给他返回nask,之后消息队列会重新把这个消息发给我们。然后我们一直消费失败,然后他就一直重试,这样的话就会无限循环,导致消息队列处理消息的内存的压力非常大,这样就不合适了。

所以说Spring又提供了消费者失败重试机制,在消费者出现异常时利用本地重试,而不是无限制的重新入队。我们也会引入相应的失败处理策略:

①就是重试次数耗尽后就直接丢弃了。

②就是重试次数耗尽后就返回nask,重新入队。

③就是重试次数耗尽后将消息投递到一个指定的交换机,在这个交换机里,专门存放异常消息,这样的话人工可以后续处理。

五、其他

其次还有一些就是我们处理业务的时候一些业务相关的业务幂等性。幂等性就是说有一些业务,重复执行可能造成不一样的后果。比如:取消订单,恢复库存的业务。如果多次恢复就会出现库存重复增加的情况。退款业务。重复退款对商家而言会有经济损失。所以,我们要尽可能避免业务被重复执行。然而在实际业务场景中,由于意外经常会出现业务被重复执行的情况,例如:页面卡顿时频繁刷新导致表单重复提交、服务间调用的重试、MQ消息的重复投递。所以我们必须想办法保证业务的幂等性。

常用有两个方案:

①唯一消息ID,这个思路非常简单,每一条消息都生成一个唯一的id,与消息一起投递给消费者。消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到redis,如果下次又收到相同消息,去redis查询判断是否存在,存在则为重复消息放弃处理。给消息添加唯一ID其实很简单,SpringAMQP的MessageConverter自带了MessageID的功能,我们只要开启这个功能即可。

②具体业务判断,比如说是,把订单状态从未支付修改为已支付。因此我们就可以在执行业务时判断订单状态是否是未支付,如果不是则证明订单已经被处理过,无需重复处理。这就是根据具体的业务来添加数据处理的条件。

虽然经过这三个步骤,其实我们还是不能完全实现消息队列的可靠性,我们通常还会设置一个兜底的方案。比如说我们的业务就是支付,然后支付完成之后,去修改订单状态为已支付。但是如果前三种也就是我们的整个的执行流程中都没有保证这个消息的可靠性,此时我们就可以设置一个定时任务,去定时检查一下订单是否已支付,如果是已支付的话就给这个订单状态改成已支付,这就是我们的一个兜底的方案。

通过这些方案其实大致是可以保证消息队列发送消息的可靠性。

相关推荐
百花~42 分钟前
Spring IoC&DI~
java·后端·spring
独自破碎E1 小时前
矩阵区间更新TLE?试试二维差分
java·线性代数·矩阵
卷到起飞的数分1 小时前
20.Spring Boot原理2
java·spring boot·后端
shepherd1111 小时前
一文带你掌握MyBatis-Plus代码生成器:从入门到精通,实现原理与自定义模板全解析
java·spring boot·后端
sivdead1 小时前
Agent平台消息节点输出设计思路
后端·python·agent
盼哥PyAI实验室1 小时前
【超详细教程】Python 连接 MySQL 全流程实战
python·mysql·oracle
程序员西西1 小时前
作为开发,你真的懂 OOM 吗?实测 3 种场景,搞懂 JVM 崩溃真相
java·后端
棒棒的皮皮1 小时前
【OpenCV】Python图像处理之按位逻辑运算
图像处理·python·opencv·计算机视觉
拾贰_C1 小时前
【ML|DL |python|pytorch|】基础学习
pytorch·python·学习