RabbitMQ 如何保证消息的可靠性以及解决幂等性

1.RabbitMQ 消息发送机制

消息大致流程:

  1. 消息先到达交换机
  2. 然后根据指定的路由规则
  3. 由交换机将消息路由到不同Queue(队列)中,由不同的消费者去消费
    RabbitMQ 如何保证消息的可靠性以及解决幂等性_第1张图片
    所以要保证消息的可靠性,就是要保证:
    1. 消息成功的到达交换机 Exchange
    2. 消息成功的到达 Queue

如果能够确认这两步,则认为消息发送成功了。
如果这两步中任意一步骤出现了问题,那么消息就没有成功的投递。此时我们应该通过重试等方式去重新发送消息,多次重试之后,如果消息还是不能到达,则可能需要人工介入了。

经过上面的分析,要确保消息成功投递,需要确保:

  1. 确认消息到达 Exchange 交换机
  2. 确认消息到达 Queue 队列
  3. 开启定时任务,定时投递那些发送失败的消息

2.解决方案

2.1、如何保证消息成功到达RabbitMQ?

  1. 开启事务机制
  2. 发送方确认机制

第一种事务的方式会影响RabbitMQ的性能,不推荐。这里讲解第二种方式!

2.2、在配置文件中配置消息发送方确认机制

spring.rabbitmq.publisher-confirm-type=cirrelated
spring.rabbitmq.publisher-returns=true
  1. 第一行表示,消息到达交换机确认回调
  2. 第二行表示,消息到达队列的回调

如果消息到达交换机会触发第一个回调,如果消息投递到对应的队列会触发第二个回调。

spring.rabbitmq.publisher-confirm-type 的配置有三个取值:

  1. none:禁用发布确认模式,默认。
  2. correlated:成功发布消息到交换机后会触发的回调方法
  3. simple:类似correlated,并且支持 waitForConfirms() 和 waitForConfirmsOrDie() 的方法调用

2.2、实现两个监听:

package com.yj.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Slf4j
@Component
public class MessageConfirmReturnCallback implements RabbitTemplate.ReturnsCallback, RabbitTemplate.ConfirmCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
      log.info("MessageReturnCallback returnedMessage={}",returnedMessage);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        log.info("MessageReturnCallback confirm={},{},{}",correlationData,b,s);
    }

    @PostConstruct
    public void initCallBack(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

}

解释:

  1. 定义配置类,实现RabbitTemplate.ConfirmCallback RabbitTemplate.ReturnsCallback 两个接口。前者用来确定消息到达交换机,后者则会在消息路由到队列失败时调用。
  2. 定义initCallBack 方法添加@PostConstruct 注解,然后在RabbitTemplate 配置这两个callback。

3、失败重试

失败重试两种情况:

  1. 没有找到MQ 导致的失败重试
  2. 找到了MQ,但是消息发送失败了

3.1、自带重试机制

如果发送方一开始就连不上MQ,那么Spring Boot 中有相应的重试机制,但是这个重试机制和MQ 本身没有关系。这是利用Spring 中的 retry 机制来完成的。具体配置如下。

# 开启重试机制
spring.rabbitmq.template.retry.enabled=true
# 重试起始间隔时间
spring.rabbitmq.template.retry.initial-interval=1000ms
# 最大重试次数
spring.rabbitmq.template.retry.max-attempts=10
# 最大重试间隔时间
spring.rabbitmq.template.retry.max-interval=10000ms
# 间隔时间乘数 (第一次间隔时间1s,第二次重试间隔时间2s,第三次4s,以此类推)
spring.rabbitmq.template.retry.multiplier=2

配置完成后,再次启动Spring Boot项目,然后关掉MQ,此时尝试发送消息,就会发送失败。进而导致自动重试。

3.2、业务重试

业务重试主要是针对消息没有到达交换机的情况。

如果消息没有成功到达交换器,根据我们第二小节的讲解,此时就会触发消息发送失败回调,在这个回调中,我们就可以做文章了!

整体思路是这样:

首先创建一张表,用来记录发送到中间件上的消息,像下面这样:
在这里插入图片描述

每次发送消息的时候,就往数据库中添加一条记录。这里的字段都很好理解,有三个我额外说下:

  • status:表示消息的状态,有三个取值,0,1,2 分别表示消息发送中、消息发送成功以及消息发送失败。
  • tryTime:表示消息的第一次重试时间(消息发出去之后,在 tryTime 这个时间点还未显示发送成功,此时就可以开始重试了)。
  • count:表示消息重试次数。
  1. 在消息发送的时候,我们就往该表中保存一条消息发送记录,并设置状态 status 为 0,tryTime 为 1 分钟之后。
  2. 在 confirm 回调方法中,如果收到消息发送成功的回调,就将该条消息的 status 设置为1(在消息发送时为消息设置 msgId,在消息发送成功回调时,通过 msgId 来唯一锁定该条消息)。
  3. 另外开启一个定时任务,定时任务每隔 10s 就去数据库中捞一次消息,专门去捞那些 status 为 0 并且已经过了 tryTime 时间记录,把这些消息拎出来后,首先判断其重试次数是否已超过 3 次,如果超过 3 次,则修改该条消息的 status 为 2,表示这条消息发送失败,并且不再重试。对于重试次数没有超过 3 次的记录,则重新去发送消息,并且为其 count 的值+1。

当然这种思路有两个弊端:

  1. 去数据库走一遭,可能拖慢 MQ 的 Qos,不过有的时候我们并不需要 MQ 有很高的 Qos,所以这个应用时要看具体情况。
  2. 按照上面的思路,可能会出现同一条消息重复发送的情况,不过这都不是事,我们在消息消费时,解决好幂等性问题就行了。

幂等性问题

幂等性产生的场景:

  1. 场景1:消费者在消费完一条消息后,向RabbitMQ 发送一个ACK 确认,但是此时网络断开或者其他原因导致RabbitMQ 没有收到这个ACK,那么RabbitMQ 并不会讲该条消息删除,而是重回队列,当客户端重新建立到连接后,消费者还是会再次收到该条消息,这就造成了消息的重复消费。
  2. 场景2:消息在发送的时候,同一条消息也可能发送多次。

解决思路:
采用Redis在消费者消费消息之前,先将消息的 id 放到 Redis 中,存储方式如下:

  • id - 0 (正在执行业务)
  • id - 1 (执行业务成功)

如果ack 失败,在RabbitMQ 将消息交给其他的消费者时,先执行setnx,如果key 已经存在(说明之前有人消费过该消息),获取它的值,如果是0,当前消费者就什么都不做。如果是1,直接ack。当消息成功消费之后,将id 对应的值设置为 1。
当前存在的极端问题:第一个消费者在执行业务时,出现了死锁,在setnx 的基础上,再给key设置一个生存时间。生产者,在发送消息时,指定messageId

你可能感兴趣的:(RabbitMQ,rabbitmq,java,分布式)