MQ高级

day05 MQ高级

消息可靠性

消息丢失的可能性

  • 发送消息时丢失:
    • 生产者发送消息时连接MQ失败
    • 生产者发送消息到达MQ后未找到Exchange
    • 生产者发送消息到达MQ的Exchange后,未找到合适的Queue
    • 消息到达MQ后,处理消息的进程发生异常
  • MQ导致消息丢失:
    • 消息到达MQ,保存到队列后,尚未消费就突然宕机
  • 消费者处理消息时:
    • 消息接收后尚未处理突然宕机
    • 消息接收后处理过程中抛出异常

要解决消息丢失问题,保证MQ的可靠性,就必须从3个方面入手:

  1. 保证生产消息的可靠性
  2. 确保MQ不会将消息弄丢
  3. 保证消费消息的可靠性

生产消息可靠性

生产者重试机制

一、生产者发送消息时,出现了网络故障,导致与MQ的连接中断

当RabbitTemplate与MQ连接超时后,多次重试。

生产者确认机制

在少数情况下,也会出现消息发送到MQ之后丢失的现象

  • MQ内部处理消息的进程发生了异常

  • 生产者发送消息到达MQ后未找到Exchange

  • 生产者发送消息到达MQ的Exchange后,未找到合适的Queue,因此无法路由

    Publisher ConfirmPublisher Return

MQ会根据消息处理的情况返回不同的回执

1.Publisher Return

消息投递成功但路由失败会调用Publisher Return回调方法返回异常信息

2.Publisher Confirm

消息投递成功返回ack,投递失败返回nack。

消息投递成功但可能路由失败了,此时会通过Publisher Confirm返回ack,通过Publisher Return回调方法返回异常信息。

publisher-confirm-type有三种模式可选:

  • none:关闭confirm机制
  • simple:同步阻塞等待MQ的回执(回调方法)
  • correlated:MQ异步回调返回回执

实现方法

ReturnCallback

每个RabbitTemplate只能配置一个ReturnCallback

ConfirmCallback

由于每个消息发送时的处理逻辑不一定相同,因此ConfirmCallback需要在每次发消息时定义

在调用RabbitTemplate中的convertAndSend方法时,多传递一个参数

convertAndSend(String,String,Object,CorrelationData)

这里的CorrelationData中包含两个核心的东西:

  • id:消息的唯一标示,MQ对不同的消息的回执以此做判断,避免混淆
  • SettableListenableFuture:回执结果的Future对象

MQ的回执就会通过这个Future来返回,给CorrelationData中的Future添加回调函数

addCallback()

发送失败处理机制

失败处理机制

将消息记录到失败消息表,由定时任务进行发布,每隔10秒钟(可设置)执行获取失败消息重新发送,发送一次则在失败次数字段加一,达到3次停止自动发送由人工处理。

当返回nack会将消息写入失败表,如果消息重发成功会将该记录从失败表删除。

当发送消息失败会入库到失败消息表。

我们可以启动定时任务去扫描失败消息表的记录,重新发送,当达到最大失败次数后由人工处理。

测试ReturnCallback时注意:单元测试方法运行完就完毕了数据库连接池,而ReturnCallback是回调方法,是在单元测试方法执行完再执行,在ReturnCallback中操作数据库时报没有可用的数据库连接的错误,需要在单元测试方法最后添加休眠代码,保证ReturnCallback执行完成再结束整个单元测试方法。

如何保证生产消息可靠性?

首先在发送消息时可以开启重试机制,避免因为短暂的网络问题导致发送消息失败。

RabbitMQ还提供生产者确认机制保证发送消息到MQ的可靠性。

生产者确认机制包括两种:

1.Publisher Return

消息投递成功但路由失败会调用Publisher Return回调方法返回异常信息。

2.Publisher Confirm

消息投递成功返回ack,投递失败返回nack。

注意:消息投递成功但可能路由失败了,此时会通过Publisher Confirm返回ack,通过Publisher Return回调方法返回异常信息。

消息持久化

为了提升性能,默认情况下MQ的数据都是在内存存储的临时数据,重启后就会消失。为了保证数据的可靠性,必须配置持久化,包括:

  • 交换机持久化
  • 队列持久化
  • 消息持久化

交换机持久化

交换机持久化是指将交换机的定义信息(元数据)持久化到RabbitMQ的数据库(mnesia)中,RabbitMQ重启后交换机定义仍然存在。

队列持久化

队列持久化也是将队列的定义信息(元数据)持久化到RabbitMQ的数据库中。

队列中存储的是消息的在队列中的位置、消息的ID、存储位置等,消息会存储在独立的rdq数据文件中,队列持久化不能保证消息数据不会丢失。

消息持久化

要确保消息不会丢失需要将消息设置为持久化。

消息的持久化是要配置一个properties

delivery_mode=2 表示持久化

消费消息可靠性

当RabbitMQ向消费者投递消息以后,需要知道消费者的处理状态如何。因为消息投递给消费者并不代表就一定被正确消费了,可能出现的故障有很多,比如:

  • 消息投递的过程中出现了网络故障
  • 消费者接收到消息后突然宕机
  • 消费者接收到消息后,因处理不当导致异常

消费者确认机制

为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)

当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ消息处理状态。回执有三种可选值:

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

SpringAMQP帮我们实现了消息确认,并可以通过配置文件设置消息确认的处理方式,有三种模式:

  • none:不处理。即消息投递给消费者后消息会立刻从MQ删除。非常不安全,不建议使用
  • manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活
  • auto:自动模式。当业务正常执行时则自动返回ack. 当业务出现异常时,根据异常判断返回不同结果:
    • 如果是业务异常,会自动返回nack;
    • 如果是消息处理或校验异常,自动返回reject,返回的异常包括:MessageConversionException、MethodArgumentTypeMismatchException等

manual模式测试(自学)

通常在应用程序中会使用auto自动模式,手动模式在生产中不常用可以自行学习。

由于手动模式需要通过api编程,需要在监听方法添加Channel、Message类型的参数

Message:是spring AMQP封装的底层消息对象。

Channel:是消费端与MQ基于通道的操作对象。

消费者确认机制怎么实现?

消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ消息处理状态:

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

具体实现方法:

SpringAMQP提供消息确认配置,有三种模式:

  • none:不处理。即消息投递给消费者后消息会立刻从MQ删除。非常不安全,不建议使用。
  • manual:手动模式。需要自己在业务代码中调用api,发送ack或reject。
  • auto:自动模式。当业务正常执行时则自动返回ack. 当业务出现异常时会自动返回nack.

我们通常使用auto自动模式,业务处理完成没有异常则自动返回ack,如果存在业务异常则自动返回nack,如果在消息处理或消息校验异常时自动返回reject。

失败重试机制

本地重试机制

消费者一直无法执行成功,那么消息投递就会无限循环,导致mq的消息处理飙升,带来不必要的压力

消费者失败重试机制:在消费者出现异常时利用本地重试,而不是无限制的投递到mq队列。

  • 消费者在失败后消息没有重新回到MQ无限重新投递,而是在本地重试了3次

  • 本地重试3次以后,查看RabbitMQ控制台,发现消息被删除了,说明最后SpringAMQP返回的是reject

  • 开启本地重试时,消息处理过程中抛出异常,不会请求到队列,而是在消费者本地重试

  • 重试达到最大次数后,Spring会返回reject,消息会被丢弃

失败消息入队

本地测试达到最大重试次数后,消息会被丢弃。

Spring允许我们自定义重试次数耗尽后的消息处理策略,这个策略是由MessageRecovery接口来定义的,它有3个不同实现:

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

比较优雅的一种处理方案是RepublishMessageRecoverer,失败后将消息投递到一个固定交换机,通过交换机将消息转发到失败消息队列,程序监听失败消息队列,接收到失败消息,将失败消息存入失败消息表,通过定时任务进行处理。

测试失败消息入队

达到最大重试次数将会投递到失败消息队列。

监听失败消息队列将失败消息写入数据库中,由人工定期处理。

消费消息失败重试机制是什么?

提供三种策略:

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

推荐使用RepublishMessageRecoverer,将失败消息投递到固定的交换机,通过交换机将消息转发到失败消息队列,程序监听失败消息队列,接收到失败消息,将失败消息存入失败消息表,通过定时任务进行处理。

MQ消息幂等性

何为幂等性?

在程序开发中,是指同一个业务,执行一次或多次对业务状态的影响是一致的。例如:

  • 根据id删除数据
  • 查询数据

但数据的更新往往不是幂等的,如果重复执行可能造成不一样的后果。比如:

  • 取消订单,恢复库存的业务。如果多次恢复就会出现库存重复增加的情况
  • 退款业务。重复退款对商家而言会有经济损失。

所以,我们要尽可能避免业务被重复执行,然而在实际业务场景中,由于意外经常会出现业务被重复执行的情况。例如:

  • 页面卡顿时频繁刷新导致表单重复提交
  • 服务间调用的重试
  • MQ消息的重复投递

因此,我们必须想办法保证消息处理的幂等性。这里给出两种方案:

  • 唯一消息ID
  • 业务状态判断

唯一消息ID

  1. 每一条消息都生成一个唯一的id,与消息一起投递给消费者。
  2. 消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库或Redis
  3. 如果下次又收到相同消息,去数据库或Redis查询判断是否存在,存在则为重复消息放弃处理。

我们该如何给消息添加唯一ID呢?

其实很简单,SpringAMQP的MessageConverter自带了MessageID的功能,我们只要开启这个功能即可。

以Jackson的消息转换器为例:

@Bean
public MessageConverter messageConverter(){
    // 1.定义消息转换器
    Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
    // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
    jjmc.setCreateMessageIds(true);
    return jjmc;
}

接收消息的方法中添加org.springframework.amqp.core.Message类型的参数,整体的处理逻辑是:

  1. 接收消息解析出消息id

  2. 根据消息id查询数据库已处理消息表如果存在说明已处理该消息,如果查询不到说明未处理该消息

  3. 根据判断结果去处理消息

  4. 处理消息完毕将消息id写入已处理消息表。

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueMessage(String msg,Channel channel,Message message) throws InterruptedException {
    log.info("spring 消费者接收到消息:【" + msg + "】");
    //消息id
    String messageId = message.getMessageProperties().getMessageId();
    //先从数据库查询是否已处理该消息,否则已处理直接return
    if (true) {
    throw new RuntimeException("故意的");
    }
    //将已处理的消息id存入数据库中...
    log.info("消息处理完成");
    }

业务判断

业务判断就是基于业务本身的逻辑或状态来判断是否是重复的请求,不同的业务场景判断的思路也不一样。

例如在支付通知案例中,处理消息的业务逻辑是把订单状态从未支付修改为已支付。因此我们就可以在执行更新时判断订单状态是否是未支付,如果不是则证明订单已经被处理过,无需重复处理。

相比较而言,使用唯一消息ID的方案需要操作数据库或Redis保存消息ID,所以更推荐使用业务判断的方案。

面试题

如何保证MQ幂等性?或 如何防止消息重复消费?

如何保证MQ幂等性?或 如何防止消息重复消费?

业务补偿

虽然我们利用各种机制尽可能增加了消息的可靠性,但也不好说能保证消息100%的可靠。万一真的MQ通知失败该怎么办呢?有没有其它补偿方案,能够确保订单的支付状态一致呢?

其实思想很简单:既然MQ通知不一定发送到交易服务,那么交易服务就必须自己主动去查询支付状态。这样即便支付服务的MQ通知失败,我们依然能通过主动查询来保证订单状态的一致。

综上,支付服务与交易服务之间的订单状态一致性是如何保证的?

  • 首先,支付服务在用户支付成功以后利用MQ消息通知交易服务,完成订单状态同步。
  • 其次,为了保证MQ消息的可靠性,我们采用了生产者确认机制、消费者确认、消费者失败重试等策略,确保消息投递的可靠性
  • 最后,我们提供了用户主动查询支付结果的入口 ,并且还在交易服务设置了定时任务,定期查询订单支付状态。这样即便MQ通知失败,还可以利用定时任务作为兜底方案,确保订单支付状态的最终一致性。
posted @ 2025-09-09 21:26  huiaxia  阅读(9)  评论(0)    收藏  举报