微服务——服务异步通讯(MQ高级)

news/2025/3/2 2:39:44/

MQ的一些常见问题

消息可靠性

生产者消息确认

返回ack,怎么感觉这么像某个tcp的3次握手。

使用资料提供的案例工程.

在图形化界面创建一个simple.queue的队列,虚拟机要和配置文件里面的一样。

 SpringAMQP实现生产者确认

AMQP里面支持多种生产者确认的类型。

simple是同步等待模式,发了消息之后就一直等待结果,可能会导致代码阻塞。

correlated是异步回调模式,像前段的ajax请求的回调函数。

ApplicationContextAware是bean工厂通知。会在Spring容器创建完后来通知并传一个spring容器到下面的方法。然后从中取到rabbitTemplate的bean并设置ReturnCallback。 

ReturnCallback:消息到了交换机,路由时失败了没有到达消息队列

ConfirmCallback:消息连交换机都没到。

这个不像ReturnCallback只能配置一个,这个可以在每次发消息时设置。

这里在发送消息时多了一个correlationData,这是在配置开关选择的confirm类型为correlated。里面封装了消息的唯一id和callback.

callback里面的result是成功的回调函数,ex是失败的回调函数。这里的失败是指回调都没收到。

实现

先是在生产者的配置文件里要加上前面的配置j

编写returnCallback

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {//获取RabbitTemplate对象RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);//配置ReturnCallbackrabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {//记录日志log.error("消息发送到队列失败,响应码:{},失败原因:{},交换机:{},路由key:{},消息:{}",replyCode,replyText,exchange,routingKey,message.toString());//如果有需要的话,可以重发消息});}
}

编写ConfirmCallback

这里先要在图形界面手动将交换机和消息队列做绑定 

    @Testpublic void testSendMessage2SimpleQueue() throws InterruptedException {//1.准备消息String message = "hello, spring amqp!";//2.准备correlationData//2.1消息IDCorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());//2.2准备ConfirmCallbackcorrelationData.getFuture().addCallback(result -> {//判断结果if(result.isAck()){//ACKlog.debug("消息成功投递到交换机!消息ID:{}",correlationData.getId());}else{//NACKlog.error("消息投递到交换机失败!消息ID:{}",correlationData.getId());}}, ex -> {//记录日志log.error("消息发送失败!",ex);//重发消息});//3.发送消息rabbitTemplate.convertAndSend("camq.topic", "simple.test", message,correlationData);}

测试得到

成功的测试情况

 

失败的测试情况

投递交换机失败,交换机不存在

投递队列失败,队列不存在

 

消息持久化

这里通过重启rabbitmq容器发现消息都不见了可以确认,rabbitmq和redis一样都是内存运行的。

甚至我手动加上的消息队列和绑定关系都不见了。这里消息队列不见是因为前面创建队列时选择的是Transient,不持久化。系统默认的交换机都还在,是因为durable为true,持久化。

创建队列或交换机的时候可以设置Durability为Durable即可持久化。

在消费者代码中进行交换机和队列的创建,然后可以看见如下持久化的交换机和队列.

@Configuration
public class CommonConfig {@Beanpublic DirectExchange simpleExchange(){return new DirectExchange("simple.direct",true,false);}@Beanpublic Queue simpleQueue(){return QueueBuilder.durable("simple.queue").build();}
}

手动发送一条消息进行测试

重启之后消息还是消失了。

要想让消息持久化,需要在发送消息时指定。

@Testpublic void testDurableMessage(){//1.准备消息Message message = MessageBuilder.withBody("hello,pop".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.PERSISTENT) //持久的.build();//2.发送消息rabbitTemplate.convertAndSend("simple.queue",message);}

 重启之后消息就持久化了。

通常在springamqp中这些都是持久化的。

消费者消息确认

在none模式下,消费者拿到消息都就报异常了,然后消息也没了。

在auto模式下,消费者拿到消息后给mq报了个unack,然后消息会重新投递,消费者继续拿消息,tmd,死循环了。 但是这里消息就不会消失了。

@RabbitListener(queues = "simple.queue")public void listenSimpleQueue(String msg) {System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");System.out.println(1/0);log.info("消费者处理消息成功!");}

消费失败重试机制

重试次数耗尽之后会将消息丢弃。

消费者失败消息处理策略

 在消费者代码中

@Configuration
public class ErrorMessageConfig {@Beanpublic DirectExchange errorMessageExchange(){return new DirectExchange("error.direct");}@Beanpublic Queue errorQueue(){return new Queue("error.queue",true);}@Beanpublic Binding errorBinding(){return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");}@Beanpublic MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");}
}

 重新发送消息进行测试,可以看见重试次数耗尽之后就送到了死信队列了。

在里面将异常的堆栈信息也包含了. 

 

死信交换机

初识死信交换机

区别在于,上一个是消费者失败之后寻找交换机路由到error队列,这个是退回到队列,再指定交换机,最后路由。

TTL

这个的应用场景比如说订单超时未支付然后自动取消。

实现  

          

准备 代码部分

    @RabbitListener(bindings = @QueueBinding(value=@Queue(name = "dl.queue",durable = "true"),exchange=@Exchange(name="dl.direct"),key = "dl"))public void listenDlQueue(String msg){log.info("接收到 dl.queue的延迟消息:{}",msg);}

@Configuration
public class TTLMessageConfig {@Beanpublic DirectExchange ttlExchange(){return new DirectExchange("ttl.direct");}@Beanpublic Queue ttlQueue(){return QueueBuilder.durable("ttl.queue").ttl(10000).deadLetterExchange("dl.direct").deadLetterRoutingKey("dl").build();}@Beanpublic Binding simpleBinging(){return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl");}
}

 测试代码

    @Testpublic void testTTLMessage(){//1.准备消息Message message = MessageBuilder.withBody("hello,ttl".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.PERSISTENT) //持久的.build();//2.发送消息rabbitTemplate.convertAndSend("ttl.direct","ttl",message);//3.记录日志log.info("消息成功发送!");}

10s之后在消费者那里就可以看见

 

 然后这里会以短的优先,5s后消费者就可以收到消息。

延迟队列

1.重装rabbitmq容器 

这个插件需要找到mq内部的插件文件夹,所以需要在创建容器的时候进行数据卷挂载。

docker run \-e RABBITMQ_DEFAULT_USER=itcast \-e RABBITMQ_DEFAULT_PASS=123321 \-v mq-plugins:/plugins \--name mq \--hostname mq1 \-p 15672:15672 \-p 5672:5672 \-d \rabbitmq:3.8-management

 2.安装DelayExchange插件

官方的安装指南地址为:Scheduling Messages with RabbitMQ | RabbitMQ - Blog

上述文档是基于linux原生安装RabbitMQ,然后安装插件。

2.1.下载插件

RabbitMQ有一个官方的插件社区,地址为:Community Plugins — RabbitMQ

大家可以去对应的GitHub页面下载3.8.9版本的插件,地址为Release v3.8.9 · rabbitmq/rabbitmq-delayed-message-exchange · GitHub这个对应RabbitMQ的3.8.5以上版本。 

查看挂载的数据卷.

docker volume inspect mq-plugins

接下来的看着好麻烦,以后看文档吧.

还真的麻烦的一批,真不想再搞这玩意,文件搞来搞去。

不知道为什么,挂载数据卷时一直报错,不能用自己定义的文件夹来挂载。

 

 

在消费者中如下声明

    @RabbitListener(bindings = @QueueBinding(value=@Queue(name = "delay.queue",durable = "true"),exchange=@Exchange(name="delay.direct",delayed = "true"),key = "delay"))public void listenDelayQueue(String msg){log.info("接收到 delay.queue的延迟消息:{}",msg);}

 在生产者中如下定义

    @Testpublic void testSendDelayMessage(){//1.准备消息Message message = MessageBuilder.withBody("hello,ttl".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.PERSISTENT) //持久的.setHeader("x-delay",5000).build();//2.准备correlationDataCorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());//3.发送消息rabbitTemplate.convertAndSend("delay.direct", "delay", message,correlationData);log.info("发送消息成功");}

测试结果如下 成功实现延迟5秒。但是会被报错,理论上说交换机应该立即转发,不会延迟,但是这里的延迟交换机可以帮忙保存消息延迟发送,所以这里才会报错,not_router,消息没有到达队列

 为了解决这个报错,需要修改生产者代码

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {//获取RabbitTemplate对象RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);//配置ReturnCallbackrabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {//判断是否是延迟消息if (message.getMessageProperties().getReceivedDelay()>0) {//是一个延迟消息,忽略错误提示return;}//记录日志log.error("消息发送到队列失败,响应码:{},失败原因:{},交换机:{},路由key:{},消息:{}",replyCode,replyText,exchange,routingKey,message.toString());//如果有需要的话,可以重发消息});}
}

惰性队列

消息堆积问题

问题解决

消费者中声明两个队列。 

@Configuration
public class LazyConfig {@Beanpublic Queue lazyQueue(){return QueueBuilder.durable("lazy.queue").lazy().build();}@Beanpublic Queue normalQueue(){return QueueBuilder.durable("normal.queue").build();}
}

 测试,准备两个队列之后分别向两个队列发消息。

    @Testpublic void testLazyMessage(){for(int i=0;i<1000000;i++){//1.准备消息Message message = MessageBuilder.withBody("hello,ttl".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT) //持久的.build();//3.发送消息rabbitTemplate.convertAndSend("lazy.queue", message);}}@Testpublic void testnormalMessage(){for(int i=0;i<1000000;i++){//1.准备消息Message message = MessageBuilder.withBody("hello,ttl".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT) //持久的.build();//3.发送消息rabbitTemplate.convertAndSend("normal.queue", message);}}

可以看见惰性队列的消息全部到paged out 刷出磁盘了?????、,为什么非惰性队列的也是刷出磁盘了。

 

MQ集群

集群个屁,不搞了.


http://www.ppmy.cn/news/1274389.html

相关文章

《Effective C++》条款35

考虑virtual函数以外的其他选择 1.Non-Virtual Interface class A { public:void test(){// 做一些事前工作dotest();// 做一些事后工作} private:virtual void dotest(){cout << "A";} }; class B :public A { public:void test(){dotest();} private:virtual…

【工作流Activiti】查询处理任务

1、查询任务 每个节点都配置了Assignee&#xff0c;流程启动后&#xff0c;任务的负责人就可以查询自己当前需要处理的任务&#xff0c;查询出来的任务都是该用户的待办任务。 Autowired private TaskService taskService;/*** 查询当前个人待执行的任务*/ Test public void f…

SpringBoot之请求的详细解析

1. 请求 在本章节呢&#xff0c;我们主要讲解&#xff0c;如何接收页面传递过来的请求数据。 1.1 Postman 之前我们课程中有提到当前最为主流的开发模式&#xff1a;前后端分离 在这种模式下&#xff0c;前端技术人员基于"接口文档"&#xff0c;开发前端程序&…

12 Vue3中的监听器

概述 Vue watchers programmatically observe component data and run whenever a particular property changes. Watched data can contain two arguments: oldVal and newVal. This can help you when writing expressions to compare data before writing or binding new v…

如何装好Home Assistant,四种方式安装HA OS测试

环境&#xff1a; 1.haos_generic-x86-64-11.1.img 2.Balena Etcher 1.18.11 3.haos_ova-11.1.qcow2 4.Ubuntu20.04 5.KVM 6.Docker version 24.0.5 7.HA OS11.2 8.联想E14笔记本 问题描述&#xff1a; 如何装好Home Assistant&#xff0c;四种方式安装HA OS测试 解决…

基于知识库的接口自动化测试——结果模型化方法与装置的分析

一、背景 随着自动化测试的设计理念不断完善、新的技术不断应用&#xff0c;自动化测试资产的积累代价和维护成本不断降低&#xff0c;自动化测试资产的数量持续增长。同时&#xff0c;随着DevOps的普及&#xff0c;应用研发过程越来越敏捷&#xff0c;自动化测试能力逐步从测…

不想学习只想摆烂系列之GUIjava项目

知道GUI框架怎么写就行 1.定义jFRame 2.分开写测试类 3.给几个按钮 4.负责提供测试器 这样就把框架打好了 主程序-继承某个类 import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener;public class RadioB…

深入理解Java中的内部类和匿名类

引言 Java中的内部类和匿名类是面向对象编程中的重要概念&#xff0c;它们提供了更灵活的方式来组织代码和实现特定的设计模式。在这篇文章中&#xff0c;我们将深入探讨这两种类的定义、用途和特点。 内部类&#xff08;Inner Classes&#xff09; 1. 成员内部类 成员内部…