RabbitMQ消息可靠性问题

RabbitMQ消息可靠性问题


发送者可靠性

生产者在发送消息的时候,可能因为网络波动、业务异常导致消息发送未成功的现象,导致消息丢失

生产者重连

有的时候由于网络波动,可能会出现客户端连接MQ失败的情况。通过配置我们可以开启连接失败后的重连机制

yaml 复制代码
spring:
	rabbitmg :
		connection-timeout: 1s #设置MQ的连接超时时间
		template:
			retry:
				enabled: true #开启超时重试机制
				initial-interval: 1000ms #失败后的初始等待时间
				multiplier: 1 #失败后下次的等待时长倍数,下次等待时长=initial-interval * multiplier
				max-attempts: 3 #最大重试次数

当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式 的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。如果对于业务性能有要求,建议禁用 重试机制。如果一定要使用,请合理配置等待时长重试次数 ,当然也可以考虑使用异步线程来执行发送消息的代码。

生产者确认

RabbitMQ提供了Publisher ConfirmPublisher Return两种确认机制。开启确机制认后,在MQ成功收到消息后会返回确认消息给生产者。返回的结果有以下几种情况:

  • 消息投递到了MQ,但是路由失败。此时会通过PublisherReturn返回路由异常原因,然后返回ACK,告知投递成功
  • 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
  • 持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知投递成功
  • 其它情况都会返回NACK,告知投递失败

生产者确认需要额外的网络和系统资源开销,尽量不要使用,如果一定要使用,无需开启Publisher-Return机制,因为一般路由失败是自己业务问题,对于nack消息可以有限次数重试,依然失败则记录异常消息


MQ可靠性

在默认情况下,RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。这样会导致两个问题:

  • 一旦MQ宕机,内存中的消息会丢失

  • 内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发MQ阻塞

    这个过程称之为PageOut,会把一部分消息暂存到磁盘中缓解消息积压,但是这个过程会导致MQ阻塞

要解决这种MQ阻塞,主要有两种方式

数据持久化

MQ数据持久化包括三个方面:交换机持久化消息队列持久化消息持久化,使用SpringAMQP创建时都会默认为持久化,因此不必过多关心

LazyQueue

从RabbitMQ的3.6.0版本开始,就增加了LazyQueue 的概念,也就是惰性队列。惰性队列的特征如下:

  • 接收到消息后直接存入磁盘而非内存(内存中只保留最近的消息,默认2048条)
  • 消费者要消费消息时才会从磁盘中读取并加载到内存
  • 支持数百万条的消息存储

在3.12版本后,所有队列都是LazyOueue模式,无法更改

那么我们现在就可以回答:RabbitMQ如何保证消息的可靠性

  • 首先通过配置可以让交换机、队列、以及发送的消息都持久化。这样队列中的消息会持久化到磁盘,MQ重启消息依然存在。
  • RabbitMO在3.6版本引入了LazyQueue,并且在3.12版本后会称为队列的默认模式。LazyQueue会将所有消息都持久化:
  • 开启持久化和生产者确认时,RabbitMQ只有在消息持久化完成后才会给生产者返回ACK回执

消费者可靠性

当生产者和MQ都正常保证了消息的可靠性,但是消费者可能因为自身的异常原因,也会导致消息丢失

消费者确认机制

为了确认消费者是否成功处理消息,RabbitMO提供了消费者确认机制(ConsumerAcknowledgement)当消费者处理消息结束后,应该向RabbitM0发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:

  • ACK:成功处理消息,RabbitMQ从队列中删除该消息
  • NACK:消息处理失败,RabbitMQ需要再次投递消息
  • Reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

那我们该怎么给MQ发送确认消息呢?

SpringAMOP已经实现了消息确认功能。并允许我们通过配置文件选择ACK处理方式,有三种方式:

  • none :不处理,即消息投递给消费者后立刻ACK,消息会立刻从MQ删除。非常不安全,不建议使用

  • manual :手动模式,需要自己在业务代码中调用api,发送ACKReject,存在业务入侵,但更灵活

  • auto :自动模式,SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ACK

    当业务出现异常时,根据异常判断返回不同结果:

    • 如果是业务异常,会自动返回NACK
    • 如果是消息处理或校验异常,自动返回Reject

消息失败处理

当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue无限循环,导致mq的消息处理飙升,带来不必要的压力

我们可以利用Sprinq的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列

yaml 复制代码
spring:
	rabbitmg:
		listener:
			simple:
				prefetch: 1
				retry:
					enabled: true #开启消费者失败重试
					initial-interval: 1000ms #初始的失败等待时长为1秒
					multiplier: 1 #下次失败的等待时长倍数,下次等待时长=multiplier *last-interval
					max-attempts: 3 #最大重试次数
					stateless: true #true无状态:false有状态。如果业务中包含事务,这里改为false

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式。
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

业务幂等性

幂等是一个数学概念,用函数表达式来描述是这样的:f(x)=f(f(x)),在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的

方案一 :是给每个消息都设置一个唯一id,利用id区分是否是重复消息:

① 每一条消息都生成一个唯一的id,与消息一起投递给消费者。

② 消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库

③ 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。

方案二:是结合业务逻辑,基于业务本身做判断。以一个订单业务为例:

我们要在支付后修改订单状态为已支付,应该在修改订单状态前先查询订单状态,判断状态是否是未支付。只有未支付订单才需要修改,其它状态不做处理,类似于CAS

相关推荐
用户8307196840822 天前
RabbitMQ vs RocketMQ 事务大对决:一个在“裸奔”,一个在“开挂”?
后端·rabbitmq·rocketmq
初次攀爬者3 天前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
初次攀爬者5 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
让我上个超影吧6 天前
消息队列——RabbitMQ(高级)
java·rabbitmq
塔中妖6 天前
Windows 安装 RabbitMQ 详细教程(含 Erlang 环境配置)
windows·rabbitmq·erlang
断手当码农6 天前
Redis 实现分布式锁的三种方式
数据库·redis·分布式
初次攀爬者6 天前
Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson
redis·分布式·后端
业精于勤_荒于稀6 天前
物流订单系统99.99%可用性全链路容灾体系落地操作手册
分布式
Ronin3056 天前
信道管理模块和异步线程模块
开发语言·c++·rabbitmq·异步线程·信道管理
Asher05096 天前
Hadoop核心技术与实战指南
大数据·hadoop·分布式