前面有一篇文章测试一下发布确认
RabbitMQ消息发布确认
现在有一个问题:
在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。 于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?特别是在这样比较极端的情况, RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?
确认机制方案
代码架构图
配置文件
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
- NONE
禁用发布确认模式,是默认值 - CORRELATED
发布消息成功到交换器后会触发回调方法 - SIMPLE
经测试有两种效果:
其一效果和 CORRELATED 值一样会触发回调方法;
其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
#配置MQ连接信息
spring.rabbitmq.addresses=192.168.235.128
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
spring.rabbitmq.publisher-confirm-type=correlated
配置类代码
package com.xiang.springboot_rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 发布确认
*/
@Configuration
public class ConfirmConfig {
//交换机
public static final String CONFIRM_EXCHANGE = "confirm_exchange";
//队列
public static final String CONFIRM_QUEUE = "confirm_queue";
//routingKey
public static final String CONFIRM_ROUTING_KEY = "key1";
//声明交换机
@Bean
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE);
}
//声明队列
@Bean
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE).build();
}
//confirmQueue绑定ConfirmExchange
@Bean
public Binding confirmQueueBindingConfirmExchange(Queue confirmQueue,DirectExchange confirmExchange){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
}
}
生产者代码
import com.xiang.springboot_rabbitmq.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试发布确认
*/
@RestController
@Slf4j
@RequestMapping("/confirm")
public class ProduceController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage")
public void sendMessage(@RequestParam String message){
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE,ConfirmConfig.CONFIRM_ROUTING_KEY,message);
log.info("发出消息:{}",message);
}
}
消费者代码
import com.rabbitmq.client.Channel;
import com.xiang.springboot_rabbitmq.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 监听 发布确认
*/
@Slf4j
@Component
public class ConfirmConsumer {
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE)
public void receiveConfirmMessage(Message message, Channel channel){
String msg = new String(message.getBody());
log.info("当前时间{},收到消息:{}",new Date().toString(),msg);
}
}
启动测试
查看日志输出
正常情况下 成功发出并消费。
但是如果MQ宕机,消息发不出去?应该把消息保存下来,确保消息不会丢失。
生产者应该感知交换机有没有收到消息。增加一个确认回调接口
回调接口
package com.xiang.springboot_rabbitmq.config;
import lombok.extern.slf4j.Slf4j;
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;
@Component
@Slf4j
public class MyConfirmCallback implements RabbitTemplate.ConfirmCallback {
//注入到RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void initialize(){
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机确认回调方法
* 1、发消息 交换机成功接收
* 2、发消息 交换机接收失败
* @param correlationData 回调消息的id以及相关信息
* @param ack 是否收到消息
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData != null ? correlationData.getId() : "";
if (ack){ //成功接收
log.info("成功接收id为:{}",id);
}else {
log.info("接收消息失败id为:{},原因:{}",id,cause);
}
}
}
要想自己的回调接口生效,需要在项目配置文件中添加
spring.rabbitmq.publisher-confirm-type=correlated
生产者改造
/**
* 测试发布确认
*/
@RestController
@Slf4j
@RequestMapping("/confirm")
public class ProduceController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage")
public void sendMessage(@RequestParam String message){
CorrelationData correlationData = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
log.info("发出消息:{}",message);
}
}
启动测试一下
执行了回调函数
为了测试收不到消息,我们将生产者发送给一个不存在的交换机进行测试,模拟MQ宕机。
import com.xiang.springboot_rabbitmq.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试发布确认
*/
@RestController
@Slf4j
@RequestMapping("/confirm")
public class ProduceController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage")
public void sendMessage(@RequestParam String message){
CorrelationData correlationData = new CorrelationData("1");
//测试发送给一个不存在的交换机
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE +"1",ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
log.info("发出消息:{}",message);
}
}
现在交换机不存在解决了,那么如果交换机正常收到消息,但是队列不存在,还会执行回调函数嘛?
测试一下
/**
* 测试发布确认
*/
@RestController
@Slf4j
@RequestMapping("/confirm")
public class ProduceController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage")
public void sendMessage(@RequestParam String message){
CorrelationData correlationData = new CorrelationData("1");
//测试发送给一个不存在的队列
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE ,ConfirmConfig.CONFIRM_ROUTING_KEY +"1",message,correlationData);
log.info("发出消息:{}",message);
}
}
交换机收到消息,执行回调,但是队列没有收到消息,导致消费者没有接收到消息。。
以看到,发送了两条消息,第一条消息的 RoutingKey 为 “key1”,第二条消息的 RoutingKey 为
RoutingKey 与队列的 BindingKey 不一致,也没有其它队列能接收这个消息,所有消息被直接丢弃了。
回退消息
Mandatory 参数
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息, 如
果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。 那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
配置文件中添加
spring.rabbitmq.publisher-returns=true
回调接口
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;
@Component
@Slf4j
public class MyConfirmCallback implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
//注入到RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void initialize(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
/**
* 交换机确认回调方法
* 1、发消息 交换机成功接收
* 2、发消息 交换机接收失败
* @param correlationData 回调消息的id以及相关信息
* @param ack 是否收到消息
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData != null ? correlationData.getId() : "";
if (ack){ //成功接收
log.info("成功接收id为:{}",id);
}else {
log.info("接收消息失败id为:{},原因:{}",id,cause);
}
}
/**
* 消息回退
* 只有消息不可达目的地的时候,才进行回退。
* @param returned
*/
@Override
public void returnedMessage(ReturnedMessage returned) {
log.info("交换机:{},消息被退回:{},退回原因:{}",
returned.getExchange(),returned.getMessage().getBody(),returned.getReplyText());
}
}
生产者代码
/**
* 测试发布确认
*/
@RestController
@Slf4j
@RequestMapping("/confirm")
public class ProduceController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage")
public void sendMessage(@RequestParam String message){
CorrelationData correlationData = new CorrelationData("1");
//测试发送给一个不存在的队列
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE ,ConfirmConfig.CONFIRM_ROUTING_KEY +"1",message,correlationData);
log.info("发出消息:{}",message);
}
}
启动测试