消息队列经常会发送失败和消费失败,这两种问题在日常工作中是不可忽视的。
消息发送失败情况:
1、网络抖动导致生产者和mq之间的连接中断,导致消息都没发。
答:rabbitmq有自动重连机制,叫retry。具体到rabbitTemplate中叫retryTemplate,可以通过设置retryTemplate来设置重连次数。
1.1、到了重连次数了,还是没连上怎么办呢?造成这种情况通常是服务器宕机等环境问题,这时候会报AmqpException,我们可以捕获这个异常,然后把消息存入缓存中。等环境正常后,做消息补发。
2、消息发了但是mq没收到,或者mq收到了但是进入到交换机之前(如果开启了消息持久化,那则是持久化之前。交换机、队列、消息默认都是持久化的)消息丢了。
答:rabbitmq有confirm机制,即mq收到消息后会发送一个叫ack的标识给生产者,ack为true表示收到了,ack为false表示没收到或丢了。rabbitTemplate中有confirmCallback,在这个callback里把ack为false的消息存到缓存,用另外线程重发。
3、消息到交换机了,但是找不到对应的queue。
答:rabbitmq有return机制,在rabbitTemplate中有returnCallback。找不到queue的消息都会进入到这个callback,在这个callback里把消息存到缓存,用另外线程重发。
消费失败情况:
消费失败也有ack机制,和生成者ack不同。我们根据处理结果返回ack(确认收到)、nack(确认未收到)、reject(拒绝)(需开启手动ack模式)。mq收到是ack的话会把消息从mq中剔除,不剔除的话mq会不断重试。
1、网络抖动、消费者代码异常、数据异常。
答:在消费者catch块里返回nack,返回nack之前还要做计数。达到规定的次数后将消息存到缓存并返回ack(因为消费者代码异常、数据异常导致的消费失败重试多少次都成功不了,不处理的话会死循环的)。
application.properties配置:
spring.rabbitmq.host=localhost
# TCP/IP端口为5672,http端口为15672
spring.rabbitmq.port=5672
spring.rabbitmq.username=root
spring.rabbitmq.password=root
# 开启发送确认
spring.rabbitmq.publisher-confirms=true
# 开启发送失败退回
spring.rabbitmq.publisher-returns=true
# 消费者ack有3种模式:NONE、AUTO、MANUAL
# NONE: 不管消费是否成功mq都会把消息剔除,这是默认配置方式。
# MANUAL:手动应答
# AUTO:自动应答,除非MessageListener抛出异常。
spring.rabbitmq.listener.direct.acknowledge-mode=manual
spring.rabbitmq.listener.simple.acknowledge-mode=manual
生产者:
package com.example.rabbitmq;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RabbitMQController {
// 这里用的是RabbitTemplate发消息,也可以用AmqpTemplate,推荐使用RabbitTemplate。
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping(value = "/helloRabbit5")
public String sendMQ5(){
String msg = "rabbitmq生成者发送失败和消费失败处理方案";
try {
// 针对网络原因导致连接断开,利用retryTemplate重连10次
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(10));
rabbitTemplate.setRetryTemplate(retryTemplate);
// 确认是否发到交换机,若没有则存缓存,用另外的线程重发,直接在里面用rabbitTemplate重发会抛出循环依赖错误
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
// 存缓存操作
System.out.println(msg + "发送失败:" + cause);
}
});
// 确认是否发到队列,若没有则存缓存,然后检查exchange, routingKey配置,之后重发
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
// 存缓存操作
System.out.println(new String(message.getBody()) + "找不到队列,exchange为" + exchange + ",routingKey为" + routingKey);
});
rabbitTemplate.convertAndSend("myExchange1", "routingKey4", msg);
} catch (AmqpException e) {
// 存缓存操作
System.out.println(msg + "发送失败:原因重连10次都没连上。");
}
return "success";
}
}
消费者:
package com.example.rabbitmq;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
@Component
public class Receiver {
/**
* basicNack(long deliveryTag, boolean multiple, boolean requeue)
* deliveryTag: 每条消息在mq内部的id,
* multiple: 是否批量(true:将一次性拒绝所有小于deliveryTag的消息);
* requeue: 是否重新入队
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "myQueue6"),
exchange = @Exchange(value = "myExchange1"),
key = "routingKey4"
))
public void process7(Message message, Channel channel) throws Exception {
// 模拟消费者代码异常,这种情况必须在catch块设置重试次数(也可以在配置文件中全局设置重试次数,当然百度的方案都不行,所以我没成功过),防止死循环
// catch块中重试可用redis的自增来做计数器,从而控制重试次数
try {
int i = 1/0;
} catch (Exception e) {
System.out.println("myQueue6:" + new String(message.getBody()));
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
// 达到重试次数后用这行代码返回ack,并将消息存缓存
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
}
启动项目,访问http://localhost:8080/helloRabbit5,当返回nack时会不断打印“myQueue6:rabbitmq生成者发送失败和消费失败处理方案”(因为我这里没有设重试次数),当返回ack时,只打印一次。