引入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); } } }