发布确认高级
1.概念
在生产环境中由于一些不明原因, 导致rabbitmq重启,在RabbitMQ重启期间生产者消息投递失败,
导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行RabitMQ的消息可靠投递呢?
特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢
2.架构
3.实现
3.1 基础部分
- 配置类
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_ROUTING_key = "confirm.routingkey";
//声明交换机
@Bean
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}
//声明普通队列
@Bean
public Queue confirmQueue(){
return new Queue(CONFIRM_QUEUE_NAME);
}
//绑定
@Bean
public Binding confirmQueueBindingconfirmExchange(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(CONFIRM_ROUTING_key);
}
}
- 生产者
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ConfirmController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMsg/{msg}")
public void sendMsg(@PathVariable String msg){
CorrelationData correlationData = new CorrelationData("1");
System.out.println(new Date() + msg);
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_key,
msg, correlationData);
CorrelationData correlationData1 = new CorrelationData("2");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_key+"01",
msg, correlationData1);
}
}
- 消费者
@Slf4j
@Component
public class ConfirmConsumer {
//接收消息
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws Exception{
String msg = new String(message.getBody());
log.info("Confirm" + new Date() + msg);
}
}
3.2 Exchange回调部分
- application.yml
NONE:禁用发布确认模式,是默认值
CORRELATED:发布消息成功到交换器后会触发回调方法
SIMPLE:
经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,
其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法
等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑, 要注意的点是
waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker
spring.rabbitmq.publisher-confirm-type= correlated
- 回调方法
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
//注入当前实现方法
public void init(){
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机确认回调方法
* 1.发消息 交换机接收到了 回调
* @param correlationData 保存回调信息的ID和相关信息
* @param b 收到/没收到信息 ack = true/false
* @param s 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData.getId();
if(b){
log.info("Success");
} else {
log.info("Failure");
}
}
}
3.3 queue回退部分
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如
果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。
那么如何让无法被路由的消息帮我想办法处理一下最起码通知我声,我好自己处理啊。
通过设置mandatory参数可以在当消息传递过程中不可达目的地时将消息返回给生产者.
- application.yml
spring.rabbitmq.publisher-returns= true
- 回调类
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
//注入当前实现方法
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData.getId();
if(b){
log.info("Success");
} else {
log.info("Failure");
}
}
//可以在当消息传递过程中不可达目的地时将消息返回给生产者
//只有不可到达时 才进行回退
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.info(returnedMessage.toString());
}
}
4.交换机备份
有了mandatory参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。
但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。
而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。
而且设置mandatory 参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。
如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?
前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。
在RabitMQ中,有一种备份交换机的机制存在,可以很好的应对这个问题。
什么是备份交换机呢?备份交换机可以理解为RabbitMQ中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,
就是为它创建一个备胎, 当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,
由备份交换机来进行转发和处理,通常备份交换机的类型为Fanout , 这样就能把所有消息都投递到与其绑
定的队列中,然后我们在备份交换机下绑定-个队列,这样所有那些原交换机无法被路由的消息,就会都,
进入这个队列了。当然,我们还可以建立一一个报警队列,用独立的消费者来进行监测和报警。
4.1 实现
- 配置类加入
//备份交换机
public static final String BACKUP_EXCHANGE_NAME = "backup_exchange";
public static final String BACK_QUEUE_NAME = "backup_queue";
public static final String WARNING_QUEUE_NAME = "warning_queue";
//声明交换机
@Bean
public DirectExchange confirmExchange(){
Map<String, Object> arg = new HashMap<>();
arg.put("alternate-exchange", BACKUP_EXCHANGE_NAME);
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true)
.withArguments(arg).build();
}
//备份交换机
@Bean
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
@Bean
public Queue backupQueue(){
return new Queue(BACK_QUEUE_NAME);
}
@Bean
public Queue warningQueue(){
return new Queue(WARNING_QUEUE_NAME);
}
@Bean
public Binding backQueueBindingbackupExchange(@Qualifier("backupQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
@Bean
public Binding warningQueueBindingbackupExchange(@Qualifier("warningQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
- 消费者
@Slf4j
@Component
public class WarningConsumer {
//接收消息
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws Exception{
String msg = new String(message.getBody());
log.info("Warning" + new Date() + msg);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南