RabbitMQ高级(三) - 消费者可靠性

104 阅读4分钟

前面两章说了生产者和 MQ 的可靠性,保障了消息从发送和保存到 MQ 的可靠性,现在消息丢失只可能是消息给了消费者,消费者自己弄丢了(出异常、没来及的处理宕机了)。
这里简单介绍以下 RabbitMQ怎么保障消费者的可靠性。

消费者确认机制

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

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

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

  • none: 不处理。即消息投递给消费者后立刻 ack,消息会立刻从 MQ 删除。非常不安全,不建议使用
  • manual: 手动模式。需要自己在业务代码中调用 api,发送 ack 或 reject,存在业务入侵,但更灵活
  • auto: 自动模式。SpringAMQP 利用 AOP 对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回 ack。
    当业务出现异常时,根据异常判断返回不同结果:
    • 如果是业务异常,会自动返回 nack

    • 如果是消息处理或校验异常,自动返回 reject

spring:
 rabbitmq:
   listener:
     direct:
       acknowledge-mode: auto

消费失败处理

当消费者出现异常后,消息会不断 requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次 requeue,无限循环,导致 mq 的消息处理飙升,带来不必要的压力。
我们可以利用 Spring 的 retry 机制,在消费者出现异常时利用本地重试,而不是无限制的 requeue 到 mq 队列:

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试机制
          initial-interval: 1000ms # 失败后的初始等待时间
          multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长=initial-interval * multiplier
          max-attempts: 3 # 最大重试次数
          stateless: true # 是否无状态 如果业务中存在事务,这里设置为 false

失败消息处理策略

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

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

将失败处理策略改为RepublishMessageRecoverer的步骤:

  1. 首先,定义接受失败消息的交换机、队列及其绑定关系。
  2. 然后,定义RepublishMessageRecoverer:
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
    return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
}

消息幂等性

通过上面的机制已经可以保障消息至少被消费一次了,因为业务时间处理过长,异常等情况,消息可能会出现重复消费,我们因该保证消息处理的一个幂等性。

幂等是一个数学概念。在程序开发中,则是指同一业务,执行一次或多次对业务状态的影响是一致的。

我们可以通过以下几种方案来保证消息处理的幂等性

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

  1. 每一条消息都生成一个唯一的id,与消息一起投递给消费者。
  2. 消费者接收到消息后处理自己的业务,业务处理成功后将消息 ID 保存到数据库。
  3. 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。
@Bean
public MessageConverter messageConverter() {
    Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
    jjmc.setCreateMessageIds(true);
    return jjmc;
}

业务判断

结合业务逻辑,基于业务本身做判断。
比如说:
我们需要在支付后修改订单状态为已支付,应该在修改订单状态的前先查询订单状态,判断状态是否是未支付。只有未支付的订单才需要修改,其他状态不做处理:

这个方案需要在代码中做判断