1.RabbitMQ 消息发送机制
消息大致流程:
- 消息先到达交换机
- 然后根据指定的路由规则
- 由交换机将消息路由到不同Queue(队列)中,由不同的消费者去消费
所以要保证消息的可靠性,就是要保证:消息成功的到达交换机 Exchange
消息成功的到达 Queue
如果能够确认这两步,则认为消息发送成功了。
如果这两步中任意一步骤出现了问题,那么消息就没有成功的投递。此时我们应该通过重试等方式去重新发送消息,多次重试之后,如果消息还是不能到达,则可能需要人工介入了。
经过上面的分析,要确保消息成功投递
,需要确保:
确认消息到达 Exchange 交换机
确认消息到达 Queue 队列
开启定时任务,定时投递那些发送失败的消息
2.解决方案
2.1、如何保证消息成功到达RabbitMQ?
- 开启事务机制
- 发送方确认机制
第一种事务的方式会影响RabbitMQ的性能,不推荐。这里讲解第二种方式!
2.2、在配置文件中配置消息发送方确认机制
spring.rabbitmq.publisher-confirm-type=cirrelated
spring.rabbitmq.publisher-returns=true
- 第一行表示,消息到达交换机确认回调
- 第二行表示,消息到达队列的回调
如果消息到达交换机会触发第一个回调,如果消息投递到对应的队列会触发第二个回调。
spring.rabbitmq.publisher-confirm-type 的配置有三个取值:
- none:禁用发布确认模式,默认。
- correlated:成功发布消息到交换机后会触发的回调方法
- 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);
}
}
解释:
- 定义配置类,实现
RabbitTemplate.ConfirmCallback
和RabbitTemplate.ReturnsCallback
两个接口。前者用来确定消息到达交换机,后者则会在消息路由到队列失败时调用。 - 定义initCallBack 方法添加@PostConstruct 注解,然后在RabbitTemplate 配置这两个callback。
3、失败重试
失败重试两种情况:
- 没有找到MQ 导致的失败重试
- 找到了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:表示消息重试次数。
- 在消息发送的时候,我们就往该表中保存一条消息发送记录,并设置状态 status 为 0,tryTime 为 1 分钟之后。
- 在 confirm 回调方法中,如果收到消息发送成功的回调,就将该条消息的 status 设置为1(在消息发送时为消息设置 msgId,在消息发送成功回调时,通过 msgId 来唯一锁定该条消息)。
- 另外开启一个定时任务,定时任务每隔 10s 就去数据库中捞一次消息,专门去捞那些 status 为 0 并且已经过了 tryTime 时间记录,把这些消息拎出来后,首先判断其重试次数是否已超过 3 次,如果超过 3 次,则修改该条消息的 status 为 2,表示这条消息发送失败,并且不再重试。对于重试次数没有超过 3 次的记录,则重新去发送消息,并且为其 count 的值+1。
当然这种思路有两个弊端:
- 去数据库走一遭,可能拖慢 MQ 的 Qos,不过有的时候我们并不需要 MQ 有很高的 Qos,所以这个应用时要看具体情况。
- 按照上面的思路,可能会出现同一条消息重复发送的情况,不过这都不是事,我们在消息消费时,解决好幂等性问题就行了。
幂等性问题
幂等性产生的场景:
- 场景1:消费者在消费完一条消息后,向RabbitMQ 发送一个ACK 确认,但是此时网络断开或者其他原因导致RabbitMQ 没有收到这个ACK,那么RabbitMQ 并不会讲该条消息删除,而是重回队列,当客户端重新建立到连接后,消费者还是会再次收到该条消息,这就造成了消息的重复消费。
- 场景2:消息在发送的时候,同一条消息也可能发送多次。
解决思路:
采用Redis
,在消费者消费消息之前,先将消息的 id 放到 Redis 中
,存储方式如下:
- id - 0 (正在执行业务)
- id - 1 (执行业务成功)
如果ack 失败,在RabbitMQ 将消息交给其他的消费者时,先执行setnx,如果key 已经存在(说明之前有人消费过该消息),获取它的值,如果是0,当前消费者就什么都不做。如果是1,直接ack。当消息成功消费之后,将id 对应的值设置为 1。
当前存在的极端问题:第一个消费者在执行业务时,出现了死锁,在setnx 的基础上,再给key设置一个生存时间。生产者,在发送消息时,指定messageId
。