MQ消息可靠性
1. 生产者消息可靠性
-
1.1 生产者重试机制
-
生产者发送消息时,出现了网络故障,导致与MQ的连接中断
-
可以进行如下配置,开启生产者重试机制
生产者重试配置
spring: rabbitmq: connection-timeout: 1s # 设置MQ的连接超时时间 template: retry: enabled: true # 开启超时重试机制 initial-interval: 1000ms # 失败后的初始等待时间 multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = 上次等待时长 * multiplier max-attempts: 3 # 总共尝试次数
-
-
1.2 生产者确认机制
-
以下情况可能会导致消息发送到MQ之后丢失:
-
MQ内部处理消息的进程发生了异常
-
生产者发送消息到达MQ后未找到Exchange
-
生产者发送消息到达MQ的Exchange后,未找到合适的Queue,因此无法路由
-
-
针对上述情况,RabbitMQ提供了生产者消息确认机制,包括Publisher Confirm和Publisher Return两种
-
Publisher Confirm:消息投递成功返回ack,投递失败返回nack
ConfirmCallback测试代码
@Test void testPublisherConfirm() { // 1.创建CorrelationData CorrelationData cd = new CorrelationData(); // 2.给Future添加ConfirmCallback cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() { @Override public void onFailure(Throwable ex) { // 2.1.Future发生异常时的处理逻辑,基本不会触发 log.error("send message fail", ex); } @Override public void onSuccess(CorrelationData.Confirm result) { // 2.2.Future接收到回执的处理逻辑,参数中的result就是回执内容 if(result.isAck()){ // result.isAck(),boolean类型,true代表ack回执,false 代表 nack回执 log.debug("发送消息成功,收到 ack!"); }else{ // result.getReason(),String类型,返回nack时的异常描述 log.error("发送消息失败,收到 nack, reason : {}", result.getReason()); } } }); // 3.发送消息,故意指定一个错误的rontingKey rabbitTemplate.convertAndSend("hmall.direct", "q", "hello", cd); }
-
Publisher Return:消息投递成功但路由失败会调用Publisher Return回调方法返回异常信息
ReturnCallback测试代码
@Slf4j @AllArgsConstructor @Configuration public class MqConfig { private final RabbitTemplate rabbitTemplate; @PostConstruct public void init(){ rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() { @Override public void returnedMessage(ReturnedMessage returned) { log.error("触发return callback,"); log.debug("exchange: {}", returned.getExchange()); log.debug("routingKey: {}", returned.getRoutingKey()); log.debug("message: {}", returned.getMessage()); log.debug("replyCode: {}", returned.getReplyCode()); log.debug("replyText: {}", returned.getReplyText()); } }); } }
-
-
1.3 发送失败处理机制
- 在ConfirmCallback中收到nack表示消息投递失败,ReturnCallback异常表示路由失败,可以将消息记录到失败消息表,由定时任务进行发布,每隔10秒钟执行获取失败消息重新发送,发送一次则在失败次数字段加一,达到3次停止自动发送由人工处理
2. 持久化
-
交换机持久化
-
交换机持久化是指将交换机的定义信息持久化到RabbitMQ的数据库中,RabbitMQ重启后交换机定义仍然存在
-
创建交换机时默认持久化
-
-
队列持久化
-
队列持久化也是将队列的定义信息持久化到RabbitMQ的数据库中
-
创建队列时默认持久化
-
-
消息持久化
-
要确保消息不会丢失需要将消息设置为持久化
-
使用springamqp发送消息时默认持久化
-
3. 消费者消息可靠性
-
3.1 消费者确认机制
-
当RabbitMQ向消费者投递消息以后,需要知道消费者的处理状态如何。因为消息投递给消费者后不一定被正确消费,可能出现很多故障导致消息丢失,比如:
-
消息投递的过程中出现了网络故障
-
消费者接收到消息后突然宕机
-
消费者接收到消息后,因处理不当导致异常
-
-
为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制,当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ消息处理状态
-
有如下三种方式实现消息确认:
-
none:不处理。即消息投递给消费者后消息会立刻从MQ删除。非常不安全,不建议使用
-
manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活
-
auto:自动模式。当业务正常执行时则自动返回ack. 当业务出现异常时,根据异常判断返回不同结果:
-
如果是业务异常,会自动返回nack
-
如果是消息处理或校验异常,自动返回reject
-
-
-
当前一般会使用auto模式,配置如下
auto模式配置
spring: rabbitmq: listener: simple: acknowledge-mode: auto # 自动ack
-
-
3.2 失败重试机制
-
当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者。如果消费者再次执行依然出错,消息会再次返回到队列,再次投递,直到消息处理成功为止
失败重试机制配置
spring: rabbitmq: listener: simple: retry: enabled: true # 开启消费者失败重试 initial-interval: 1000ms # 初识的失败等待时长为1秒 multiplier: 1 # 失败的等待时长倍数,下次等待时长 = 上次等待时长 * multiplier max-attempts: 3 # 最大重试次数
-
当前,当失败次数达到最大重试次数时,消息会被丢弃。这在某些对于消息可靠性要求较高的业务场景下不太合适
-
Spring允许自定义重试次数耗尽后的消息处理策略,这个策略是由MessageRecovery接口来定义的,有3个不同实现:
-
RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
-
ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
-
RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
-
-
可以选择第三种RepublishMessageRecoverer,失败后将消息投递到一个固定交换机,通过交换机将消息转发到失败消息队列,程序监听失败消息队列,接收到失败消息,将失败消息存入失败消息表,通过定时任务进行处理
-
-
-
3.3 消息幂等性
-
在程序开发中,消息幂等性是非常重要的,通常是指同一个业务,执行一次或多次对业务状态的影响是一致的,但数据的更新往往不是幂等的,如果重复执行可能造成不一样的后果,所以,我们要尽可能避免业务被重复执行
-
保证消息处理的幂等性有如下两种方案:
-
唯一消息ID:在监听到消息后,执行业务前,可以判断该消息的ID在redis是否存在。如果不存在,说明该业务还没有被执行,此时可以将消息ID写入到redis中,然后开始执行业务,业务执行完成后删除redis中的ID;如果消息ID在redis中存在,说明该业务正在被执行,则什么都不做
-
业务状态判断:业务判断就是基于业务本身的逻辑或状态来判断是否是重复的请求,不同的业务场景判断的思路也不一样。例如在支付通知案例中,处理消息的业务逻辑是把订单状态从未支付修改为已支付。因此我们就可以在执行更新时判断订单状态是否是未支付,如果不是则证明订单已经被处理过,无需重复处理
-
-
-
-
3.4 业务补偿
-
虽然有各种机制来增加消息的可靠性,但也不能保证消息完全可靠,此时可以通过业务逻辑来对MQ的失败进行补偿
- 比如用户在下单后完成支付,通过MQ通知订单服务更改订单状态时失败,这时就可以在用户支付完成后的支付成功弹窗页面点击支付成功按钮时,主动查询支付状态后更新订单状态
-