生产者发送消息,先将消息发送到 Exchange
,然后由 Exchange
再路由到 Queue
,这中间就需要确认两个事情
Exchange
Exchange
成功路由到 Queue
Spring
提供了两个 回调函数
来处理这两种消息发送确认
Exchange
一般会采用轻量级的 Confirm
确认机制,跟手动 Ack
机制类似。生产者将消息发送到 RabbitMQ
,且将消息持久化到硬盘后,RabbitMQ
会通过一个回调方法将 Confirm
信息回传给生产端。这样,如果生产端收到了这个 Confirm
信息,就知道是该消息已经持久化到磁盘了;否则,那么就说明这条消息可能半路丢失了,此时你就可以重新投递消息到 MQ
去,确保消息不会丢失
Confirm
确认机制实现 ConfirmCallback
并重写 confirm
回调方法。消息发送到 Broker
后触发回调,可以确认消息是否成功发送到 Exchange
# 开启 confirms 回调,确认消息是否成功发送到 Exchange
spring.rabbitmq.publisher-confirms=true
@Autowired
private CachingConnectionFactory connectionFactory;
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(converter());
// 消息是否成功发送到 Exchange 交换机
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.info("消息成功发送到Exchange");
String msgId = correlationData.getId();
msgLogService.updateStatus(msgId, Constant.MsgLogStatus.DELIVER_SUCCESS);
} else {
log.info("消息发送到Exchange失败, {}, cause: {}", correlationData, cause);
}
});
return rabbitTemplate;
}
Exchange
交换机成功路由到 Queue
队列实现 ReturnCallback
并重写 returnedMessage
回调方法,可以确认消息从 EXchange
路由到 Queue
失败。这里的回调是一个失败回调,只有消息从 Exchange
路由到 Queue
失败才会回调这个方法
# 开启 returnedMessage 回调,确认消息是否从 Exchange 成功发送到 -> Queue
spring.rabbitmq.publisher-returns=true
# 触发returnedMessage回调必须设置mandatory=true, 否则Exchange没有找到Queue就会丢弃掉消息, 而不会触发回调
spring.rabbitmq.template.mandatory=true
@Autowired
private CachingConnectionFactory connectionFactory;
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(converter());
// 消息是否成功发送到 Exchange
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.info("消息成功发送到Exchange");
String msgId = correlationData.getId();
msgLogService.updateStatus(msgId, Constant.MsgLogStatus.DELIVER_SUCCESS);
} else {
log.info("消息发送到Exchange失败, {}, cause: {}", correlationData, cause);
}
});
// 消息是否从 Exchange 成功路由到 Queue
// 注意: 这是一个失败回调, 只有消息从Exchange路由到Queue失败才会回调这个方法
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.info("消息从Exchange路由到Queue失败: exchange: {}, route: {}, replyCode: {}, replyText: {}, message: {}", exchange, routingKey, replyCode, replyText, message);
});
return rabbitTemplate;
}
RabbitMQ
默认自动确认消息被正确消费,即消息投递到消费者后就自动确认消息被处理完毕,并且会将该消息删除,即使消费者意外宕机,或者抛出异常,如果消费者接收到消息,还没处理完成就 down
掉或者抛出异常。那么,这条消息就丢失了
问题就出在 RabbitMQ
它只管将消息发送出去,而不管消息是否被正确消费了,就会自动删除消息。所以,只要将自动 Ack
确认修改为手动 Ack
确认,消费成功才会通知 RabbitMQ
可以删除该消息即可。如果消费者宕机消费失败,由于 RabbitMQ
并未收到 Ack
通知,且感知到该消费者状态异常(如抛出异常),就会将该消息重新推送给其他消费者,让其他消费者继续执行,这样就保证消费者挂掉但消息不会丢失
RabbitMQ
消息的确认模式AcknowledgeMode.NONE
:默认使用自动确认AcknowledgeMode.AUTO
:根据情况确认AcknowledgeMode.MANUAL
:手动确认# 设置消息的接收确认模式:手动确认(ack)
spring.rabbitmq.listener.simple.acknowledge-mode=manual
@Component
@Slf4j
public class LogUserConsumer {
@Autowired
UserLogService userLogService;
@RabbitListener(queues = "log.user.queue")
public void logUserConsumer(Message message, Channel channel, @Header (AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
try {
log.info("收到消息: {}", message.toString());
userLogService.insert(MessageHelper.msgToObj(message, UserLog.class));
} catch (Exception e){
log.error("logUserConsumer error", e);
// ack返回false,并重新回到队列
channel.basicNack(tag, false, true);
} finally {
// 告诉服务器,收到这条消息,已经被我消费了。可以在队列删掉,这样以后就不会再发了,否则消息服务器以为这条消息没处理掉,后续还会在发
channel.basicAck(tag, false);
}
}
}
channel.basicAck(tag, false)
方法, 第一个参数deliveryTag
(唯一标识 ID
):当一个消费者向 RabbitMQ
注册后,会建立起一个 Channel
,RabbitMQ
会用 basic.deliver
方法向消费者推送消息,这个方法携带了一个 deliverytag
,它代表了 RabbitMQ
向该 Channel
投递的这条消息的唯一标识 ID
,是一个单调递增的正整数,deliverytag
的范围仅限于 Channel
multiple
:为了减少网络流量,手动确认可以被批处理,当该参数为 true
时,则可以一次性确认 delivery_tag
小于等于传入值的所有消息RabbitMQ
消息的持久化:是将 Exchange、Queue
和 message
都持久化到硬盘。这样 RabbitMQ
重启时,会把持久化的 Exchange、Queue
和 message
从硬盘重新加载出来,重新投递消息
Exchange
的持久化声明交换机时指定持久化参数为 true
即可
@Bean
public DirectExchange logUserExchange() {
return new DirectExchange("log.user.exchange", true, false);
}
第二个参数 durable
是否持久化,第三个参数 autoDelete
当所有绑定队列都不再使用时,是否自动删除交换器,true
删除,false
不删除
Queue
的持久化声明队列时指定持久化参数为 true
即可
@Bean
public Queue logUserQueue() {
return new Queue("log.user.queue.name", true);
}
Message
的持久化是通过配置 deliveryMode
实现的,生产者投递时,指定 deliveryMode
为 MessageDeliveryMode.PERSISTENT
即可实现消息的持久化。投递和消费都需要通过 Message
对象进行交互,为了不每次都写配置转换的代码,我们写一个消息帮助类 MessageHelper
public class MessageHelper {
public static Message objToMsg(Object obj) {
if (null == obj) {
return null;
}
Message message = MessageBuilder.withBody(JsonUtil.objToStr(obj).getBytes()).build();
// 消息持久化
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
message.getMessageProperties().setContentType(MessageProperties.CONTENT_TYPE_JSON);
return message;
}
public static <T> T msgToObj(Message message, Class<T> clazz) {
if (null == message || null == clazz) {
return null;
}
String str = new String(message.getBody());
T obj = JsonUtil.strToObj(str, clazz);
return obj;
}
}
消息投递时:
rabbitTemplate.convertAndSend("log.user.exchange.name", "log.user.routing.key.name", MessageHelper.objToMsg(userLog));
消息消费时(参考二、消息接收确认)
UserLog userLog = MessageHelper.msgToObj(message, UserLog.class);
如果不需要消息持久化,则不需要通过 Message
进行转换, 可以直接通过字符串或者对象投递和消费
unack
消息的积压问题什么叫 unack
消息的积压问题,简单来说就是消费者处理能力有限,无法一下将 MQ
投递过来的所有消息消费完,如果 MQ
推送消息过多,比如可能有几千上万条消息积压在某个消费者实例内存中,此时这些积压的消息就处于 unack
状态,如果一直积压,就有可能导致消费者服务实例内存溢出、内存消耗过大、甚至内存泄露。所以, RabbitMQ
是必须要考虑一下消费者服务的处理能力的
如何解决? RabbitMQ
基于一个 prefetchcount
来控制这个 unack message
的数量。你可以通过 channel.basicQos(10)
这个方法来设置当前 channel
的 prefetchcount
。也可以通过配置文件设置
spring.rabbitmq.listener.simple.prefetch=10
10
的话,那么意味着当前这个 channel
里,unack message
的数量不能超过 10
个,以此来避免消费者服务实例积压 unack message
过多RabbitMQ
正在投递到 channel
过程中的 unack message
,以及消费者服务在处理中的 unack message
,以及异步 ack
之后还没完成 ack
的 unack message
,所有这些 message
加起来,一个 channel
也不能超过 10
个如果你要简单粗浅的理解的话,也大致可以理解为这个 prefetch count
就代表了一个消费者服务同时最多可以获取多少个 message
来处理。prefetch
就是预抓取的意思,就意味着你的消费者服务实例预抓取多少条 message
过来处理,但是最多只能同时处理这么多消息
如果一个 channel
里的 unack message
超过了 prefetch count
指定的数量,此时 RabbitMQ
就会停止给这个 channel
投递消息了,必须要等待已经投递过去的消息被 ack了,此时才能继续投递下一个消息
设置多大合理? RabbitMQ
官方给出的建议是 prefetch count
一般设置在100 - 300
之间。也就是一个消费者服务最多接收到100 - 300
个 message
来处理,允许处于 unack
状态。这个状态下可以兼顾吞吐量也很高,同时也不容易造成内存溢出的问题
# rabbitmq
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 开启confirms回调 Product -> Exchange
spring.rabbitmq.publisher-confirms=true
# 开启returnedMessage回调 Exchange -> Queue
spring.rabbitmq.publisher-returns=true
# 触发returnedMessage回调必须设置mandatory=true, 否则Exchange没有找到Queue就会丢弃掉消息, 而不会触发回调
spring.rabbitmq.template.mandatory=true
# 设置手动确认(ack) Queue -> Consumer
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.simple.prefetch=100
# 是否开启消费者重试(为false时关闭消费者重试,这时消费端代码异常会一直重复收到消息)
spring.rabbitmq.listener.simple.retry.enabled=true
# 最大重试次数
spring.rabbitmq.listener.simple.retry.max-attempts=3
# 重试间隔时间(单位毫秒)
spring.rabbitmq.listener.simple.retry.initial-interval=5000
# 重试最大时间间隔(单位毫秒)
spring.rabbitmq.listener.simple.retry.max-interval=1200000
# 应用于前一重试间隔的乘法器
spring.rabbitmq.listener.simple.retry.multiplier=5