目录
一、可靠性传输

可以从以下几个方面考虑
1、生产者------Broker
可能原因:网络问题等
解决办法:
producer------exchange:发送方确认-confirm模式(消息接收成功ack=true,消息接受失败ack=false)
2、Exchange------Queue
可能原因:代码或者配置错误,导致消息路由失败
解决办法:发送发确认-returns模式(出现错误退回给发送者,并且附带cause)
3、队列溢出:死信等
4、Broker内部出错
可能原因:消息到达RabbitMQ之后,RabbitMQ Server宕机导致消息丢失
解决办法:持久化操作
(1)交换机持久化
(2)队列持久化
(3)消息持久化
注:消息持久化必须和队列持久化搭配,如果队列非持久化,那么就算消息是持久化的,消息也会丢失,只有队列和消息同时都是持久化,消息才不会丢失
1.交换机:持久化:durable(true) 数据不丢失非持久化:durable(false) 数据丢失
2.队列持久化(队列不丢失):
消息持久化:durable(Constant.PRES_QUEUE)
MessageDeliveryMode.PERSISTENT 消息不丢失
消息非持久化:durable(Constant.PRES_QUEUE)
MessageDeliveryMode.NON_PERSISTENT 消息丢失
3.队列非持久化(队列元数据丢失):
消息持久化:nonDurable(Constant.PRES_QUEUE)
MessageDeliveryMode.PERSISTENT 消息丢失
消息非持久化:nonDurable(Constant.PRES_QUEUE)
MessageDeliveryMode.NON_PERSISTENT 消息丢失
5、消费者异常,导致消息丢失
可能原因:消息到达消费者,还没来得及消费,消费者宕机、消费者逻辑有问题
解决办法:消息确认(默认是自动确认,可以开启手动确认避免消息丢失)、重试机制
(1)自动确认
(2)手动确认
二、死信队列

1、死信队列概念:
死信队列是存储死信消息的队列。
死信(Dead Letter)是消息队列中一种特殊消息,他指的是无法被正常消费或处理的消息。
2、死信来源
(1)消息过期:消息在队列中存活时间超过了设定的TTL
eg:假如我们设置消息存活时间为10s,向normal_queue队列发送消息

等待10s之后观察到,消息进入到了死信队列

(2)消息被拒绝:消费者处理消息的时候,会因为一些原因(消息内容错误、处理逻辑异常)拒绝处理该消息,如果拒绝时指定不重新入队的话(requeue=false),就会变成死信消息
java
public class DlxQueueListener {
//指定监听的队列
@RabbitListener(queues= Constant.NORMAL_QUEUE)
public void listener(Message message, Channel channel) throws IOException {
long deliveryTag=message.getMessageProperties().getDeliveryTag();
try {
System.out.printf("接收到消息:%s,deliveryTag: %d%n",
new String(message.getBody(),"UTF-8"),message.getMessageProperties().getDeliveryTag());
int num = 3/0;
System.out.println("处理完成");
//手动确认,
//basicAck需要两个参数,deliveryTag和multiple,是否批量
channel.basicAck(deliveryTag,false);
} catch (IOException e) {
//出现异常就否定确认,三个参数,第三个是否重新入队,如果第三个参数设置为false,就会变成死信
channel.basicNack(deliveryTag,false,false);
}
}
//然后监听一下死信队列,查看死信队列到底能否收到这个消息
@RabbitListener(queues = Constant.DL_QUEUE)
public void dlListener(Message message, Channel channel) throws UnsupportedEncodingException {
System.out.printf("接收到消息:%s,deliveryTag: %d%n",
new String(message.getBody(),"UTF-8"),message.getMessageProperties().getDeliveryTag());
}
}
(3)队列满了:队列达到最大长度,无法容纳新消息时,新来的消息会被处理成死信
eg:我们设置队列最大长度为10L,向队列中发送消息超过10条消息,可以看到超过队列长度的消息都进入了死信队列

3、死信队列应用场景
异常情况下,消息进入死信队列,应用程序可以在启动一个消费者,去消费死信队列里面的内容来分析当时遇到的异常情况,进而改善和优化系统。
消息重试:将死信消息重新发送到原队列或另一个队列重试处理;
消息丢弃:直接丢弃这些无法处理的消息,以免占用资源;
日志收集:将死信消息作为日志收集起来,用于后续分析和问题定位;
三、延迟队列
1、概念:
延迟队列是指:消息发送之后,并不会立即给消费者,而是等待特定的时间,才发送给消费者。
2、应用场景:
(1)订单在规定时间内未支付会自动取消;
(2)用户注册之后,3天后发送调查问卷;
(3)用户发起退款,24小时之后商家未处理,则默认同意,自动退款
3、实现方式:
(1)TTL+死信队列实现:
优点:灵活不需要安装插件即可使用
缺点:存在消息顺序问题(比如先发送30s的TTL消息,在发送10s的TTL消息,那么10s的TTL消息,会在30s过期之后才进行处理);需要额外的逻辑来处理死信队列的消息,增加系统的复杂性
(2)基于插件实现的延迟队列
优点:通过插件可以直接创建延迟队列,简化延迟消息的实现;避免了DLX的时序问题
缺点:需要依赖特定的插件,有运维工作;只适用特定版本
四、幂等性问题
1、幂等性概念:
简单来说,幂等性就是一个操作,无论你执行多少次,产生的影响和只执行一次的影响是一样的。
举个例子:
select操作 :select执行一次或者多次对数据状态的最终影响都是一样的,**这里要注意,select操作的查询结果可能不一样,因为上一次查询和下一次查询之间如果对数据进行别的操作,那返回的结果也不一样,但是这个结果的不一样,不是select造成的,**所以select操作是幂等性的;
i++操作:i++是非幂等性的,因为每执行一次i++,都会使结果改变一次
2、MQ的幂等性
核心矛盾: RabbitMQ 为了确保消息绝不丢失,提供了 "至少一次" 的保障。这意味着,它可能会因为各种原因(如网络抖动、消费者处理成功但返回确认信号失败)把同一条消息发送多次给消费者。
一般消息中间件的消息传输保障分为三个层级:
最多一次:消息可能丢失,但绝不会重复传输
至少一次:消息绝不会丢失,但可能会重复传输
恰好一次:每条消息肯定会被传输一次且只传输一次
带来的问题:如果消费者处理的是一个非幂等操作(比如扣款、核销优惠券、减少库存),那么同一条消息被处理多次,就会造成严重事故(比如扣两次款)。
因此,MQ的幂等性指的是: 即使消费者收到了多条相同的消息,整个系统(尤其是数据库)的最终状态也只会被改变一次,就像只收到一条消息一样。
3、保证幂等性的方案
全局唯一ID(最常用、最有效):
(1)生产端:在生产者发送消息时,给每条消息加一个全局唯一的ID(比如UUID)
(2)消费端:消费者收到消息之后,在处理业务逻辑之前,先拿着这个ID查一下(可以在读写很快的数据库,如Redis上查询)
(3)判断处理:如果查不到ID,说明是新消息,就进行业务处理,处理完将这个ID标记为已处理(写入Redis中);如果查到这个ID在Redis已经存在,说明之前处理过,那就直接丢弃,不进行业务处理
使用Redis的setnx(key,value)来写入,因为这个操作具有原子性,只有key不存在的时候才可以设置成功
业务逻辑判断:
这个方法不依赖额外的ID,而是在进行业务处理之前,先去数据库里面查询是否已经存在相关数据记录(和前面全局ID查询不同,这里查的是业务数据表,业务状态和约束)或者使用乐观锁机制避免更新已被其他事务更改的数据。
五、顺序性保证
1、顺序性保证介绍
一句话概括: 生产者按照顺序 消息A -> 消息B -> 消息C 发送消息,消费者也严格按照 消息A -> 消息B -> 消息C 的顺序进行消费和处理。
2、RabbitMQ默认难以保证顺序性
(1)多个消费者:这是打破顺序性的最主要原因。配置了多个消费者时,消息可能会被不同的消费者并行处理,从而导致消息处理的顺序性无法保证;
(2)网络波动:在消息传递过程中,如果出现网络波动或异常,可能会导致消息确认(ACK)丢失,从而使得消息被重新入队和重新消费,造成顺序性问题;
(3)消息重试:如果消费者在处理消息后未能及时发送确认,或者确认消息在传输过程中丢失,那么MQ可能会认为消息未被成功消费而进行重试,这也可能导致消息处理的顺序性问题.
(4)消息路由问题:在复杂的路由场景中,消息可能会根据路由键被发送到不同的队列,从而无法保证全局的顺序性;
(5)死信队列:消息因为某些原因(如消费端拒绝消息)被放入死信队列,死信队列被消费时,无法保证消息的顺序和生产者发送消息的顺序一致
3、顺序性保证方案
单队列单消费者:由于只有一个消费者,消息会严格按照FIFO(先进先出)的顺序被取出和处理,前一条消息处理完,后一条才会被处理。
优点:实现简单、保证顺序
缺点:吞吐量低,无法利用并发优势,容易成为系统瓶颈
适用场景:消息量不大,但对顺序要求极高的场景
分区消费:这是前一个方案的扩展,兼顾顺序性和吞吐量,将需要保证顺序的消息进行分区,确保同一组内消息由同一个消费者顺序处理(实现分区可以借助业务逻辑或者spring-cloud-stream 来实现)
消息确认机制:使用手动消息确认机制,消费者在统一处理完一条消息后,显式地发送确认,这样RabbitMQ才会移除并继续发送下一条消息
业务逻辑控制:当无法在MQ层面实现绝对顺序时,可能在消费端通过业务逻辑来保障最终结果的正确性,比如在消息中嵌入序列号,并在消费时根据信息来处理
六、消息积压问题
1、消息积压概念:
消息积压指的是:待处理消息的数量超过了消费者的处理能力,导致消息在RabbitMQ的队列中不断堆积,无法被及时消费的情况。
2、常见原因分析
消息生产过快:业务高峰期、定时任务集中出发、程序bug导致循环发送消息,这使得消息的入口流量瞬间或持续远高于系统处理能力
消费者处理能力不足:
(1)业务逻辑复杂:消息处理中包含耗时的CPU密集计算或频繁的I/O操作,导致消费者跟不上生产者的速度;
(2)消费者实例不足:部署的消费者服务实例数量太少,无法对应上涨的流量;
(3)代码bug或异常处理不当:消费代码存在bug导致消息处理失败,从而触发无限重试,或者未设置合理的重试次数和死信队列,失败的消息始终堆积在主队列中
(4)资源瓶颈:消费者服务站所在的资源(CPU\内存、磁盘I/O、网络带宽)耗尽,导致服本身处理缓慢
网络问题:由于网络延迟或不稳定,消费者无法及时接收或确认消息,最终导致消息积压
3、解决方案
提高消费者效率:
增加消费者实例数量;
优化业务逻辑;
设置prefetchCount,当一个消费者阻塞时,消息转发到其他未阻塞的消费者;
消息发生异常时,设置合适的重试机制,或者转入死信队列里面
限制生产者速率:
流量控制:在消息生产者中实现流量控制逻辑,根据消费者处理能力动态调整发送频率;
限流:使用限流工具,为消息发送速率设置一个上限;
设置过期时间:如果消息过期未消费,可以配置死信队列,避免消息丢失
资源配置优化:
升级服务器的硬件,调整RabbitMQ的配置参数等