回调函数是指当消息发送到交换机或队列是的回调通知。
spring:
profiles:
active: dev
# rabbitmq 配置
rabbitmq:
host: 10.10.11.21
port: 5672
username: guest
password: guest
virtual-host: / # 设置虚拟主机,不设置则使用默认host
# 消息确认配置项
# 确认消息已发送到交换机: Exchange
publisher-confirm-type: correlated
# 确认消息已发送到队列: Queue
publisher-returns: true
/**
* 配置回调
*/
@Configuration
public class RabbitConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
// 没有找到交换机的回调,没有找到队列也会调用这个回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("ConfirmCallback: "+"相关数据:"+correlationData);
System.out.println("ConfirmCallback: "+"确认情况:"+ack);
System.out.println("ConfirmCallback: "+"原因:"+cause);
}
});
// 没有找到队列的回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("ReturnCallback: "+"消息:"+message);
System.out.println("ReturnCallback: "+"回应码:"+replyCode);
System.out.println("ReturnCallback: "+"回应信息:"+replyText);
System.out.println("ReturnCallback: "+"交换机:"+exchange);
System.out.println("ReturnCallback: "+"路由键:"+routingKey);
}
});
return rabbitTemplate;
}
}
上面写了两个回调函数,一个叫 ConfirmCallback ,一个叫 RetrunCallback;
推送消息存在四种情况:
①消息推送到server,但是在server里找不到交换机
②消息推送到server,找到交换机了,但是没找到队列
③消息推送到sever,交换机和队列啥都没找到
④消息推送成功
经过测试得出结论:
①这种情况触发的是 ConfirmCallback 回调函数。
②这种情况触发的是 ConfirmCallback和RetrunCallback两个回调函数。
③这种情况触发的是 ConfirmCallback 回调函数。
④这种情况触发的是 ConfirmCallback 回调函数。
和生产者的消息确认机制不同,因为消息接收本来就是在监听消息,符合条件的消息就会消费下来。
所以,消息接收的确认机制主要存在三种模式:
①自动确认
, 这也是默认的消息确认情况。 AcknowledgeMode.NONE
RabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。
所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。
一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。
② 根据情况确认
, 这个不做介绍
③ 手动确认
, 这个比较关键,也是我们配置接收消息确认机制时,多数选择的模式。
消费者收到消息后,手动调用basic.ack/basic.nack/basic.reject后,RabbitMQ收到这些消息后,才认为本次投递成功。
basic.ack
用于肯定确认
basic.nack
用于否定确认(注意:这是AMQP 0-9-1的RabbitMQ扩展)
basic.reject
用于否定确认,但与basic.nack相比有一个限制:一次只能拒绝单条消息
消费者端以上的3个方法都表示消息已经被正确投递,但是basic.ack表示消息已经被正确处理。
而basic.nack,basic.reject表示没有被正确处理。
channel.basicReject(deliveryTag, true)
; 拒绝消费当前消息,如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行。 下次不想再消费这条消息了。
使用拒绝后重新入列这个确认模式要谨慎,因为一般都是出现异常的时候,catch异常再拒绝入列,选择是否重入列。
但是如果使用不当会导致一些每次都被你重入列的消息一直消费-入列-消费-入列这样循环,会导致消息积压.
channel.basicNack(deliveryTag, false, true)
;
第一个参数依然是当前消息到的数据的唯一id;
第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。
同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。
spring:
profiles:
active: dev
# rabbitmq 配置
rabbitmq:
host: 10.10.11.21
port: 5672
username: guest
password: guest
virtual-host: / # 设置虚拟主机,不设置则使用默认host
# 消息确认配置项
# 确认消息已发送到交换机: Exchange
publisher-confirm-type: correlated
# 确认消息已发送到队列: Queue
publisher-returns: true
listener:
simple:
acknowledge-mode: manual # 采用手动应答, auto:自定应答
default-requeue-rejected: false
// 消费者监听
@Configuration
public class DirectReceiver {
@RabbitListener(queues = "test-direct-queue") //监听队列的名称
public void processDirect(Message message, Channel channel) throws IOException {
// 采用手动应答模式,手动确认应答更加安全稳定
System.out.println("direct监听消息===第一个====:"+ new String(message.getBody()));
// channel.basicAck(message.getMessageProperties().getDeliveryTag(),true); // 确认消费
// channel.basicNack(deliveryTag, false, true);
/**
* 设置不消费某条消息
* //第一个参数依然是当前消息到的数据的唯一id;
* //第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
* //第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去
*/
// channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
/**
* 拒绝消费
* 如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。设置false,
* 就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行。 下次不想再消费这条消息了
*/
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:
那么该消息将成为“死信”,“死信”消息会被RabbitMQ进行特殊处理。如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。
配置的方法就是在声明队列的时候,添加参数 x-dead-letter-exchange
及 x-dead-letter-routing-key
,其实就是在消费失败时,将消息使用该 exchange 及 routing 发送至指定队列
/**
* @Description:
* @ClassName TopicRabbitConfig
* @date: 2021.08.23 09:52
* @Author: zhanghang
*/
@Configuration
public class TopicRabbitConfig {
// 正常的topic 队列
public static final String TEST_TOPIC_EXCHANGE_NAME = "test_topic_exchange_name";
public static final String TEST_TOPIC_QUEUE_NAME = "test_topic_queue_name";
public static final String TEST_TOPIC_ROUTING_NAME = "test_topic_routing_name";
// topic 死信队列
public static final String TEST_DEAD_TOPIC_EXCHANGE_NAME = "test_dead_topic_exchange_name";
public static final String TEST_DEAD_TOPIC_QUEUE_NAME = "test_dead_topic_queue_name";
public static final String TEST_DEAD_TOPIC_ROUTING_NAME = "test_dead_topic_routing_name";
// 正常的队列
@Bean
public Queue createTopicQueue(){
Map<String, Object> arguments = new HashMap<>(2);
// 绑定死信交换机
arguments.put("x-dead-letter-exchange", TEST_DEAD_TOPIC_EXCHANGE_NAME);
// 绑定死信的路由key
arguments.put("x-dead-letter-routing-key", TEST_DEAD_TOPIC_ROUTING_NAME);
// 绑定死信队列的交换机和路由
return new Queue(TEST_TOPIC_QUEUE_NAME,true,false,false,arguments);
}
@Bean
public TopicExchange createTopicExchange(){
// 参数说明:
// name: 交换机名称
// durable: 是否持久化
// autoDelete: 是否自动删除
return new TopicExchange(TEST_TOPIC_EXCHANGE_NAME,true,false);
}
@Bean
public Binding createTopicBinding(){
return BindingBuilder.bind(createTopicQueue()).to(createTopicExchange()).with(TEST_TOPIC_ROUTING_NAME);
}
// 死信队列
@Bean
public Queue createDeadTopicQueue(){
return new Queue(TEST_DEAD_TOPIC_QUEUE_NAME,true,false,false);
}
@Bean
public TopicExchange createDeadTopicExchange(){
// 参数说明:
// name: 交换机名称
// durable: 是否持久化
// autoDelete: 是否自动删除
return new TopicExchange(TEST_DEAD_TOPIC_EXCHANGE_NAME,true,false);
}
@Bean
public Binding createDeadTopicBinding(){
return BindingBuilder.bind(createDeadTopicQueue()).to(createDeadTopicExchange()).with(TEST_DEAD_TOPIC_ROUTING_NAME);
}
}
发送消息
@GetMapping("/topic/sendMsg")
public String topicSendMsg(){
String msg = "hello World";
rabbitTemplate.convertAndSend("test_topic_exchange_name","test_topic_routing_name",msg);
return "ok~";
}
监听消息
@Component
public class TestTopicListener {
/**
* description: 正常消费的队列
* date: 2021年-08月-23日 10:30
* author: zhanghang
*
* @param msg
* @param channel
* @return void
*/
@RabbitListener(queues = "test_topic_queue_name")
public void receiver(Message msg, Channel channel, @Headers Map<String,Object> headers) throws IOException, InterruptedException {
try {
//打印数据
String message = new String(msg.getBody(), StandardCharsets.UTF_8);
System.out.println("receiver消费消息{}"+message);
System.out.println("headers{}"+headers);
// channel.basicReject(msg.getMessageProperties().getDeliveryTag(), false);
int i = 1/0; // 主动抛出异常
channel.basicAck(msg.getMessageProperties().getDeliveryTag(),true);
}catch (Exception e){
System.out.println("receiver发生异常,拒绝重新入队{}");
channel.basicReject(msg.getMessageProperties().getDeliveryTag(),false);
}
}
// 监听死信队列
@RabbitListener(queues = "test_dead_topic_queue_name")
public void receiverDead(Message msg, Channel channel, @Headers Map<String,Object> headers) throws IOException, InterruptedException {
//打印数据
String message = new String(msg.getBody(), StandardCharsets.UTF_8);
System.out.println("receiverDead消费消息{}"+message);
System.out.println("headers{}"+headers);
channel.basicAck(msg.getMessageProperties().getDeliveryTag(),true);
}
}
经过测试发现。当正常的消费者拒绝消息就会进入到死信队列。