引入RabbitMQ后,如何保证全链路数据100%不丢

参考:https://mp.weixin.qq.com/s/iiFMwD0thp3SDooRGhtWgA

我们都知道,消息从生产端到消费端消费要经过3个步骤:

1、生产端发送消息到RabbitMQ;

2、RabbitMQ发送消息到消费端;

3、消费端消费这条消息;

 这3个步骤中的每一步都有可能导致消息丢失,消息丢失不可怕,可怕的是丢失了我们还不知道,所以要有一些措施来保证系统的可靠性。这里的可靠并不是一定就100%不丢失了,磁盘损坏,机房爆炸等等都能导致数据丢失,当然这种都是极小概率发生,能做到99.999999%消息不丢失,就是可靠的了。下面来具体分析一下问题以及解决方案。

一、生产端可靠性投递

生产端可靠性投递,即生产端要确保将消息正确投递到RabbitMQ中。生产端投递的消息丢失的原因有很多,比如消息在网络传输的过程中发生网络故障消息丢失,或者消息投递到RabbitMQ时RabbitMQ挂了,那消息也可能丢失,而我们根本不知道发生了什么。针对以上情况,RabbitMQ本身提供了一些机制。

1.1、事务消息机制

事务消息机制由于会严重降低性能,所以一般不采用这种方法,我就不介绍了,而采用另一种轻量级的解决方案——confirm消息确认机制。

1.2、confirm消息确认机制

什么是confirm消息确认机制?顾名思义,就是生产端投递的消息一旦投递到RabbitMQ后,RabbitMQ就会发送一个确认消息给生产端,让生产端知道我已经收到消息了,否则这条消息就可能已经丢失了,需要生产端重新发送消息了。

通过下面配置来开启确认模式:

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual#消费手动确认
        prefetch: 1
        retry:
            enable: true #支持失败重试
            max-attempts: 5#最大重试次数
            max-interval:20000ms#最大间隔时间
            initial-interval:3000ms# 重试间隔时间 3秒
            multiplier:2#乘子  重试间隔*乘子得出下次重试间隔  3s  6s  12s  24s  此处24s>20s  走20s
        default-requeue-rejected:false#重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列)
    publisher-confirms: true #开启消息确认机制
    publisher-returns: true #支持消息发送失败返回队列

通过下面代码监听确认和未确认的消息

@Component
@slfj
public class TopicSend implements RabbitTemplate.ConfirmCallback { @Autowired private RabbitTemplate rabbitTemplate; public void send(String msg){ rabbitTemplate.setConfirmCallback(this); CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); log.info("发送的消息为:{}",msg); this.rabbitTemplate.convertAndSend("topicExchange","topic",msg,correlationData); } @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { log.info("消息id:{}" , correlationData.getId());
if (ack) { log.info("消息发送确认成功"); } else { log.error("消息发送确认失败:{}" , cause); } } }

这样就可以让生产端感知到消息是否投递到RabbitMQ中了,当然这样还不够,稍后我会说一下极端情况。

 1.3、消息持久化

我们知道,RabbitMQ收到消息后将这个消息暂时存在了内存中,那这就会有个问题,如果RabbitMQ挂了,那重启后数据就丢失了,所以相关的数据应该持久化到硬盘中,这样就算RabbitMQ重启后也可以到硬盘中取数据恢复。那如何持久化呢?

message消息到达RabbitMQ后先是到exchange交换机中,然后路由给queue队列,最后发送给消费端。

所以需要给exchange、queue和message都进行持久化:

@RabbitListener(
        bindings = @QueueBinding(
                value = @Queue(value = "${mq.config.queue.error}",autoDelete = "false"),
                exchange = @Exchange(value = "${mq.config.exchange}", type = ExchangeTypes.DIRECT),
                key = "${mq.config.queue.error.routing.key}"
        )
)

在@Queue和@Exchange注解中都有autoDelete属性,值是布尔类型的字符串。如:autoDelete=“false”。

 @Queue:当所有消费客户端断开连接后,是否自动删除队列: true:删除,false:不删除。

@Exchange:当所有绑定队列都不在使用时,是否自动删除交换器: true:删除,false:不删除。

这样,如果RabbitMQ收到消息后挂了,重启后会自行恢复消息。

到此,RabbitMQ提供的几种机制都介绍完了,但这样还不足以保证消息可靠的投递RabbitMQ中,上面我也提到了会有极端情况,比如RabbitMQ收到消息还没来得及将消息持久化到硬盘时,RabbitMQ挂了,这样消息还是丢失了,或者RabbitMQ在发送确认消息给生产端的过程中,由于网络故障而导致生产端没有收到确认消息,这样生产端就不知道RabbitMQ到底有没有收到消息,就不好做接下来的处理。

所以除了RabbitMQ提供的一些机制外,我们自己也要做一些消息补偿机制,以应对一些极端情况。接下来我就介绍其中的一种解决方案——消息入库。

1.4、消息入库

首先发送消息前先将消息保存到数据库中,有一个状态字段status=0,表示生产端将消息发送给了RabbitMQ但还没收到确认;在生产端收到确认后将status设为1,表示RabbitMQ已收到消息。这里有可能会出现上面说的两种情况,所以生产端这边开一个定时器,定时检索消息表,将status=0并且超过固定时间后(可能消息刚发出去还没来得及确认这边定时器刚好检索到这条status=0的消息,所以给个时间)还没收到确认的消息取出重发(第二种情况下这里会造成消息重复,消费者端要做幂等性),可能重发还会失败,所以可以做一个最大重发次数,超过就做另外的处理。

这样消息就可以可靠性投递到RabbitMQ中了,而生产端也可以感知到了。

二、消费端消息不丢失

既然已经可以让生产端100%可靠性投递到RabbitMQ了,那接下来就改看看消费端的了,如何让消费端不丢失消息。

默认情况下,以下3种情况会导致消息丢失:

(1)在RabbitMQ将消息发出后,消费端还没接收到消息之前,发生网络故障,消费端与RabbitMQ断开连接,此时消息会丢失;

(2)在RabbitMQ将消息发出后,消费端还没接收到消息之前,消费端挂了,此时消息会丢失;

(3)消费端正确接收到消息,但在处理消息的过程中发生异常或宕机了,消息也会丢失。

 

其实,上述3中情况导致消息丢失归根结底是因为RabbitMQ的自动ack机制,即默认RabbitMQ在消息发出后就立即将这条消息删除,而不管消费端是否接收到,是否处理完,导致消费端消息丢失时RabbitMQ自己又没有这条消息了。

所以就需要将自动ack机制改为手动ack机制。

spring.rabbitmq.listener.simple.acknowledge-mode=manual

SpringBoot集成RabbitMQ确认机制分为三种:none、auto(默认)、manual()

@RabbitListener(queues = AMQEnum.MY_TO_DO_QUEUE)
    public void getMsg(Message message, Channel channel){
  try{
     //处理逻辑
    ....
    // 确认收到消息,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息
       channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
  }catch(Exception e){
    if (message.getMessageProperties().getRedelivered()) {
                log.info("消息已重复处理失败,拒绝再次接收" + user.getName());
                // 拒绝消息,requeue=false 表示不再重新入队,如果配置了死信队列则进入死信队列
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
            } else {
               / requeue为是否重新回到队列,true重新入队
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            }
 }
}    
posted @ 2021-12-27 17:49  炫舞风中  阅读(129)  评论(0编辑  收藏  举报