消息大致流程:
消息成功的到达交换机 Exchange
消息成功的到达 Queue
如果能够确认这两步,则认为消息发送成功了。
如果这两步中任意一步骤出现了问题,那么消息就没有成功的投递。此时我们应该通过重试等方式去重新发送消息,多次重试之后,如果消息还是不能到达,则可能需要人工介入了。
经过上面的分析,要确保消息成功投递
,需要确保:
确认消息到达 Exchange 交换机
确认消息到达 Queue 队列
开启定时任务,定时投递那些发送失败的消息
第一种事务的方式会影响RabbitMQ的性能,不推荐。这里讲解第二种方式!
spring.rabbitmq.publisher-confirm-type=cirrelated
spring.rabbitmq.publisher-returns=true
如果消息到达交换机会触发第一个回调,如果消息投递到对应的队列会触发第二个回调。
spring.rabbitmq.publisher-confirm-type 的配置有三个取值:
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
两个接口。前者用来确定消息到达交换机,后者则会在消息路由到队列失败时调用。失败重试两种情况:
如果发送方一开始就连不上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,此时尝试发送消息,就会发送失败。进而导致自动重试。
业务重试主要是针对消息没有到达交换机的情况。
如果消息没有成功到达交换器,根据我们第二小节的讲解,此时就会触发消息发送失败回调,在这个回调中,我们就可以做文章了!
整体思路是这样:
每次发送消息的时候,就往数据库中添加一条记录。这里的字段都很好理解,有三个我额外说下:
当然这种思路有两个弊端:
幂等性产生的场景:
- 场景1:消费者在消费完一条消息后,向RabbitMQ 发送一个ACK 确认,但是此时网络断开或者其他原因导致RabbitMQ 没有收到这个ACK,那么RabbitMQ 并不会讲该条消息删除,而是重回队列,当客户端重新建立到连接后,消费者还是会再次收到该条消息,这就造成了消息的重复消费。
- 场景2:消息在发送的时候,同一条消息也可能发送多次。
解决思路:
采用Redis
,在消费者消费消息之前,先将消息的 id 放到 Redis 中
,存储方式如下:
如果ack 失败,在RabbitMQ 将消息交给其他的消费者时,先执行setnx,如果key 已经存在(说明之前有人消费过该消息),获取它的值,如果是0,当前消费者就什么都不做。如果是1,直接ack。当消息成功消费之后,将id 对应的值设置为 1。
当前存在的极端问题:第一个消费者在执行业务时,出现了死锁,在setnx 的基础上,再给key设置一个生存时间。生产者,在发送消息时,指定messageId
。